Re: [PATCH v2] posix: Fix wordexp WRDE_APPEND to preserve state on non-NOSPACE errors (BZ 34090, CVE-2026-6368)
Adhemerval Zanella Netto <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Organization | Linaro |
| Message-ID | <[email protected]> |
On 08/07/26 18:59, DJ Delorie wrote: > > A few tests could have more coverage, and one question about a return > path. > > Adhemerval Zanella <[email protected]> writes: >> +/* w_addword grows we_wordv with realloc, make every call guaranteed to >> + relocate the block. This makes BZ 34090 regression more deterministic. */ >> +void * >> +realloc (void *ptr, size_t size) >> +{ >> + if (ptr == NULL) >> + return malloc (size); >> + if (size == 0) >> + { >> + free (ptr); >> + return NULL; >> + } >> + >> + void *new = malloc (size); >> + if (new == NULL) >> + return NULL; >> + >> + /* Copy only what is valid in the old block to avoid reading past it. */ >> + size_t old = malloc_usable_size (ptr); >> + memcpy (new, ptr, old < size ? old : size); >> + free (ptr); >> + return new; >> +} > > Ok. Might be useful to fill the old chunk with junk before freeing, but > I don't see how it would help this test. Ack, I think it won't hurt and it might trigger invalid read if the code read stale pointers. > >> +/* Verify that all words in we match the expected NULL-terminated >> + array. */ >> +static void >> +check_words (const wordexp_t *we, const char *const *expected, int line) >> +{ >> + size_t i; >> + for (i = 0; expected[i] != NULL; i++) >> + { >> + TEST_VERIFY (i < we->we_wordc); >> + TEST_COMPARE_STRING (we->we_wordv[we->we_offs + i], expected[i]); >> + } >> + TEST_COMPARE (we->we_wordc, i); >> +} > > What is the "line" argument here for? It was left-over from development, I will remove it (it helps a bit debugging possible issues, but at same time I do not want to add another TEST_COMPARE_ to parametrize it or open-code the macro). > >> +#define CHECK_WORDS(we, ...) \ >> + do { \ >> + const char *const expected_[] = { __VA_ARGS__, NULL }; \ >> + check_words (we, expected_, __LINE__); \ >> + } while (0) > > Ok. > >> +/* Test 1: WRDE_APPEND + WRDE_BADCHAR preserves we_wordc. */ >> +static void >> +test_append_badchar_preserves_count (void) >> +{ >> + printf ("info: test_append_badchar_preserves_count\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("one two three", &we, 0), 0); >> + TEST_COMPARE (we.we_wordc, 3); >> + >> + size_t saved_count = we.we_wordc; >> + >> + /* ')' triggers WRDE_BADCHAR and "extra" would be a new word if the >> + expansion succeeded, exercising the w_addword path before the error >> + is detected. */ >> + TEST_COMPARE (wordexp ("extra )", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (we.we_wordc, saved_count); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 2: WRDE_APPEND + WRDE_BADCHAR preserves the we_wordv pointer even >> + when internal realloc would move the buffer. */ >> +static void >> +test_append_badchar_preserves_pointer (void) >> +{ >> + printf ("info: test_append_badchar_preserves_pointer\n"); >> + wordexp_t we = { 0 }; >> + >> + /* Use many words so that the initial we_wordv allocation is >> + non-trivial and a later realloc is more likely to move it. */ >> + TEST_COMPARE (wordexp ("a b c d e f g h", &we, 0), 0); >> + TEST_COMPARE (we.we_wordc, 8); >> + >> + char **saved_wordv = we.we_wordv; >> + size_t saved_count = we.we_wordc; >> + >> + /* The interposed realloc guarantees the internal we_wordv buffer moves >> + during parsing, so the pointer-stability check below is meaningful. */ >> + TEST_COMPARE (wordexp ("append )", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (we.we_wordc, saved_count); >> + TEST_VERIFY (we.we_wordv == saved_wordv); >> + >> + wordfree (&we); >> +} > > The test should verify that realloc was actually called. Ack, I will a test for that. > >> +/* Test 3: After a failed WRDE_APPEND the original words are still accessible >> + and correct. */ >> +static void >> +test_append_badchar_words_intact (void) >> +{ >> + printf ("info: test_append_badchar_words_intact\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("alpha beta gamma", &we, 0), 0); >> + CHECK_WORDS (&we, "alpha", "beta", "gamma"); >> + >> + TEST_COMPARE (wordexp ("delta )", &we, WRDE_APPEND), WRDE_BADCHAR); >> + >> + /* Words must still be intact. */ >> + CHECK_WORDS (&we, "alpha", "beta", "gamma"); >> + /* The NULL terminator must still be present. */ >> + TEST_VERIFY (we.we_wordv[we.we_offs + we.we_wordc] == NULL); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 4: Successful WRDE_APPEND still works (regression test). */ >> +static void >> +test_append_success (void) >> +{ >> + printf ("info: test_append_success\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("hello", &we, 0), 0); >> + TEST_COMPARE (we.we_wordc, 1); >> + >> + TEST_COMPARE (wordexp ("world", &we, WRDE_APPEND), 0); >> + TEST_COMPARE (we.we_wordc, 2); >> + CHECK_WORDS (&we, "hello", "world"); >> + >> + wordfree (&we); >> +} > > Should test that the pointer actually changed, too. Ack. > >> +/* Test 5: Successful append after a failed append — the implementation must >> + recover and allow further use of the wordexp_t. */ >> +static void >> +test_append_success_after_failure (void) >> +{ >> + printf ("info: test_append_success_after_failure\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("first", &we, 0), 0); >> + CHECK_WORDS (&we, "first"); >> + >> + TEST_COMPARE (wordexp ("bad |", &we, WRDE_APPEND), WRDE_BADCHAR); >> + >> + /* State must be exactly as before the failed call. */ >> + CHECK_WORDS (&we, "first"); >> + >> + /* A subsequent successful append must work. */ >> + TEST_COMPARE (wordexp ("second third", &we, WRDE_APPEND), 0); >> + CHECK_WORDS (&we, "first", "second", "third"); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 6: Multiple consecutive failed appends do not corrupt state. */ >> +static void >> +test_append_multiple_failures (void) >> +{ >> + printf ("info: test_append_multiple_failures\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("keep this", &we, 0), 0); >> + CHECK_WORDS (&we, "keep", "this"); >> + >> + size_t saved_count = we.we_wordc; >> + char **saved_wordv = we.we_wordv; >> + >> + /* Each of these bad characters must leave the state unchanged. */ >> + TEST_COMPARE (wordexp ("x )", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (wordexp ("x |", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (wordexp ("x ;", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (wordexp ("x &", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (wordexp ("x <", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (wordexp ("x >", &we, WRDE_APPEND), WRDE_BADCHAR); >> + >> + TEST_COMPARE (we.we_wordc, saved_count); >> + TEST_VERIFY (we.we_wordv == saved_wordv); >> + CHECK_WORDS (&we, "keep", "this"); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 7: WRDE_APPEND with WRDE_SYNTAX error (unterminated quote) also >> + preserves state. */ >> +static void >> +test_append_syntax_error (void) >> +{ >> + printf ("info: test_append_syntax_error\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("original", &we, 0), 0); >> + CHECK_WORDS (&we, "original"); >> + >> + char **saved_wordv = we.we_wordv; >> + size_t saved_count = we.we_wordc; >> + >> + /* Unterminated double quote triggers WRDE_SYNTAX. */ >> + TEST_COMPARE (wordexp ("\"unterminated", &we, WRDE_APPEND), WRDE_SYNTAX); >> + >> + TEST_COMPARE (we.we_wordc, saved_count); >> + TEST_VERIFY (we.we_wordv == saved_wordv); >> + CHECK_WORDS (&we, "original"); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 8: Error without WRDE_APPEND still works (regression test for the >> + non-APPEND code path in do_error). */ >> +static void >> +test_no_append_error (void) >> +{ >> + printf ("info: test_no_append_error\n"); >> + wordexp_t we = { 0 }; >> + >> + /* Simple failure without WRDE_APPEND. */ >> + TEST_COMPARE (wordexp ("bad |", &we, 0), WRDE_BADCHAR); >> + >> + /* After failure without WRDE_APPEND the struct should be safe to >> + reuse — start fresh. */ >> + TEST_COMPARE (wordexp ("ok", &we, 0), 0); >> + CHECK_WORDS (&we, "ok"); >> + >> + wordfree (&we); >> +} > > Should there be a test that "we" is unchanged? I think the > wordexp/check is sufficient. Ok. Agreed. > >> +/* Test 9: WRDE_BADCHAR on the very first character (no partial words added >> + before the error). */ >> +static void >> +test_append_badchar_immediate (void) >> +{ >> + printf ("info: test_append_badchar_immediate\n"); >> + wordexp_t we = { 0 }; >> + >> + TEST_COMPARE (wordexp ("hello world", &we, 0), 0); >> + CHECK_WORDS (&we, "hello", "world"); >> + >> + char **saved_wordv = we.we_wordv; >> + size_t saved_count = we.we_wordc; >> + >> + /* The bad character is the very first byte — no w_addword call happens >> + before the error. */ >> + TEST_COMPARE (wordexp ("|", &we, WRDE_APPEND), WRDE_BADCHAR); >> + TEST_COMPARE (we.we_wordc, saved_count); >> + TEST_VERIFY (we.we_wordv == saved_wordv); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 10: WRDE_APPEND into an empty wordexp_t (initial call uses WRDE_APPEND >> + with a zeroed struct — unusual but allowed). */ >> +static void >> +test_append_into_empty (void) >> +{ >> + printf ("info: test_append_into_empty\n"); >> + wordexp_t we = { 0 }; >> + >> + /* First call with WRDE_APPEND on a zeroed struct. The implementation >> + must handle we_wordv == NULL gracefully. */ >> + TEST_COMPARE (wordexp ("solo", &we, WRDE_APPEND), 0); >> + TEST_COMPARE (we.we_wordc, 1); >> + CHECK_WORDS (&we, "solo"); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Verify that the leading we_offs slots are all NULL. */ >> +static void >> +check_offs_null (const wordexp_t *we) >> +{ >> + for (size_t i = 0; i < we->we_offs; i++) >> + TEST_VERIFY (we->we_wordv[i] == NULL); >> +} > > Ok. > >> +/* Test 11: successful WRDE_APPEND with WRDE_DOOFFS and a non-zero we_offs. >> + The leading offset slots must stay NULL and words must land at >> + we_wordv[we_offs + i] across both the initial and the appended call. */ >> +static void >> +test_dooffs_append_success (void) >> +{ >> + printf ("info: test_dooffs_append_success\n"); >> + wordexp_t we = { 0 }; >> + we.we_offs = 2; >> + >> + TEST_COMPARE (wordexp ("one two", &we, WRDE_DOOFFS), 0); >> + TEST_COMPARE (we.we_offs, 2); >> + check_offs_null (&we); >> + CHECK_WORDS (&we, "one", "two"); >> + >> + TEST_COMPARE (wordexp ("three", &we, WRDE_APPEND | WRDE_DOOFFS), 0); >> + TEST_COMPARE (we.we_offs, 2); >> + check_offs_null (&we); >> + CHECK_WORDS (&we, "one", "two", "three"); >> + /* The NULL terminator must sit right after the last word. */ >> + TEST_VERIFY (we.we_wordv[we.we_offs + we.we_wordc] == NULL); >> + >> + wordfree (&we); >> +} > > Ok. > >> +/* Test 12: failed WRDE_APPEND with WRDE_DOOFFS preserves we_wordc, the >> + we_wordv pointer, the words and the leading NULL offset slots. This >> + exercises the we_offs arithmetic in the array duplication and in the >> + error-path cleanup (we_wordv[we_offs + --we_wordc]). */ >> +static void >> +test_dooffs_append_error_preserves_state (void) >> +{ >> + printf ("info: test_dooffs_append_error_preserves_state\n"); >> + wordexp_t we = { 0 }; >> + we.we_offs = 3; >> + >> + TEST_COMPARE (wordexp ("alpha beta", &we, WRDE_DOOFFS), 0); >> + check_offs_null (&we); >> + CHECK_WORDS (&we, "alpha", "beta"); >> + >> + char **saved_wordv = we.we_wordv; >> + size_t saved_count = we.we_wordc; >> + >> + /* "gamma" is a partial word added via w_addword (forcing a relocating >> + realloc of we_wordv) before ')' triggers WRDE_BADCHAR. */ >> + TEST_COMPARE (wordexp ("gamma )", &we, WRDE_APPEND | WRDE_DOOFFS), >> + WRDE_BADCHAR); >> + >> + TEST_COMPARE (we.we_offs, 3); >> + TEST_COMPARE (we.we_wordc, saved_count); >> + TEST_VERIFY (we.we_wordv == saved_wordv); >> + check_offs_null (&we); >> + CHECK_WORDS (&we, "alpha", "beta"); >> + TEST_VERIFY (we.we_wordv[we.we_offs + we.we_wordc] == NULL); >> + >> + wordfree (&we); >> +} > > Ok. > > >> +static int >> +do_test (void) >> +{ >> + test_append_badchar_preserves_count (); >> + test_append_badchar_preserves_pointer (); >> + test_append_badchar_words_intact (); >> + test_append_success (); >> + test_append_success_after_failure (); >> + test_append_multiple_failures (); >> + test_append_syntax_error (); >> + test_no_append_error (); >> + test_append_badchar_immediate (); >> + test_append_into_empty (); >> + test_dooffs_append_success (); >> + test_dooffs_append_error_preserves_state (); >> + >> + return 0; >> +} >> + >> +#include <support/test-driver.c> > > Ok. > >> diff --git a/posix/wordexp.c b/posix/wordexp.c > >> + /* When WRDE_APPEND is set we work on a copy of the we_wordv array so that >> + the caller's original pointer is never invalidated by realloc inside >> + w_addword. The saved_wordv keeps the original; on success we free it, >> + on non-NOSPACE error we free the working copy and restore the original. */ >> + char **saved_wordv = NULL; > > Ok. > > >> + else if (pwordexp->we_wordv != NULL) >> + { >> + /* WRDE_APPEND with an existing word list: duplicate the array so that >> + realloc during parsing does not invalidate the caller's pointer. The >> + strings themselves are shared. */ >> + size_t num_p; >> + char **dup; >> + if (INT_ADD_WRAPV (pwordexp->we_offs, pwordexp->we_wordc, &num_p) >> + || INT_ADD_WRAPV (num_p, 1, &num_p)) >> + return WRDE_NOSPACE; >> + dup = __libc_reallocarray (NULL, num_p, sizeof *dup); > > I assume we rely on reallocarray to do its own overflow check, so... > >> + if (dup == NULL) >> + return WRDE_NOSPACE; >> + memcpy (dup, pwordexp->we_wordv, num_p * sizeof *dup); > > We don't need to do it here. > >> + saved_wordv = pwordexp->we_wordv; >> + pwordexp->we_wordv = dup; >> + } > > Ok. > >> error = w_addword (pwordexp, NULL); >> >> if (error) >> - return error; >> + goto do_error; >> } > > Ok. > >> if (error) >> - return error; >> + goto do_error; >> } > > Ok. > >> >> /* There was a word separator at the end */ >> if (word == NULL) /* i.e. w_newword */ >> - return 0; >> + { >> + free (saved_wordv); >> + return 0; >> + } > > Ok. > >> /* There was no field separator at the end */ >> - return w_addword (pwordexp, word); >> + error = w_addword (pwordexp, word); >> + free (saved_wordv); >> + return error; > > Is there a possible error here that would require us to preserve the > original array? I don't think so, w_addword can only fail with WRDE_NOSPACE, and for it the words successfully expanded are keppt. However, double checking this w_addword call might leak word on failure, I will fix it. > >> if (error == WRDE_NOSPACE) >> - return WRDE_NOSPACE; >> + { >> + /* we_wordc and we_wordv are updated to reflect any words that were >> + successfully expanded. The old array is obsolete. */ >> + free (saved_wordv); >> + return WRDE_NOSPACE; >> + } > > Ok. > >> - if ((flags & WRDE_APPEND) == 0) >> - wordfree (pwordexp); >> + if (flags & WRDE_APPEND) >> + { >> + /* POSIX 2024 states that for in other error cases, if the WRDE_APPEND >> + flag was specified, we_wordc and we_wordv shall not be modified. >> + >> + Free strings appended during this call, discard the working copy of >> + we_wordv, and restore the caller's original pointer. */ >> + while (pwordexp->we_wordc > old_word.we_wordc) >> + free (pwordexp->we_wordv[pwordexp->we_offs + --pwordexp->we_wordc]); >> + free (pwordexp->we_wordv); >> + pwordexp->we_wordv = saved_wordv; >> + } >> + else >> + { >> + wordfree (pwordexp); >> + *pwordexp = old_word; >> + } >> >> - *pwordexp = old_word; >> return error; >> } > > Ok. >