From e956e857dd7466696dc2bbefe7611a3e48c6de32 Mon Sep 17 00:00:00 2001 From: DJ Delorie Date: Wed, 26 Aug 2026 22:54:43 -0400 Subject: [PATCH] CVE-2026-6368: wordexp WRDE_APPEND state rollback on error (RHEL-238480) Resolves: RHEL-238480 RPM-Skip-Release: yes --- glibc-RHEL-238480.patch | 572 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 572 insertions(+) create mode 100644 glibc-RHEL-238480.patch diff --git a/glibc-RHEL-238480.patch b/glibc-RHEL-238480.patch new file mode 100644 index 0000000..2c0203a --- /dev/null +++ b/glibc-RHEL-238480.patch @@ -0,0 +1,572 @@ +commit e2cefe16c37a617df9f11407cb00a272a6098823 +Author: Adhemerval Zanella +Date: Mon Apr 13 16:33:30 2026 -0300 + + posix: Fix wordexp WRDE_APPEND to preserve state on non-NOSPACE errors (BZ 34090, CVE-2026-6368) + + The previous implementation saved a copy of the wordexp_t struct at + entry and blindly restored it on error via (*pwordexp = old_word). + This is incorrect when WRDE_APPEND is set because w_addword may have + called realloc on we_wordv during partial processing before the error + was detected. If realloc relocated the buffer, the saved we_wordv + pointer is dangling; restoring it causes a use-after-free in the + caller (e.g. via wordfree), and the relocated buffer is leaked. + + Fix this by duplicating the we_wordv pointer array at entry when + WRDE_APPEND is set, so that all subsequent realloc calls inside + w_addword operate on the copy. + + This change also fixes a POSIX conformance issue: if the WRDE_APPEND + flag is specified, pwordexp->we_wordc and pwordexp->we_wordv shall + not be modified. + + Also fix two pre-existing error return paths in the '"' and '\'' cases + that returned directly from w_addword failures instead of going through + do_error, which would leak the saved array (and previously would also + skip the word cleanup). + + Checked on x86_64-linux-gnu and i686-linux-gnu. + + Reviewed-by: DJ Delorie + +Conflicts: + posix/Makefile + context for old testcase order + posix/wordexp.c + context and line numbers + +diff --git a/posix/Makefile b/posix/Makefile +index d9f897faa1..10d70ae0ae 100644 +--- a/posix/Makefile ++++ b/posix/Makefile +@@ -97,6 +97,7 @@ tests := test-errno tstgetopt testfnm r + tst-sysconf-empty-chroot tst-glob_symlinks tst-fexecve \ + tst-glob-tilde test-ssize-max tst-spawn4 bug-regex37 \ + bug-regex38 tst-regcomp-truncated tst-regcomp-bracket-free \ ++ tst-wordexp-append \ + tst-wordexp-reuse + tests-internal := bug-regex5 bug-regex20 bug-regex33 \ + tst-rfc3484 tst-rfc3484-2 tst-rfc3484-3 \ +diff --git a/posix/tst-wordexp-append.c b/posix/tst-wordexp-append.c +new file mode 100644 +index 0000000000..87f388f0a7 +--- /dev/null ++++ b/posix/tst-wordexp-append.c +@@ -0,0 +1,393 @@ ++/* Test for wordexp with WRDE_APPEND flag. ++ Copyright (C) 2026 Free Software Foundation, Inc. ++ This file is part of the GNU C Library. ++ ++ The GNU C Library is free software; you can redistribute it and/or ++ modify it under the terms of the GNU Lesser General Public ++ License as published by the Free Software Foundation; either ++ version 2.1 of the License, or (at your option) any later version. ++ ++ The GNU C Library is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ Lesser General Public License for more details. ++ ++ You should have received a copy of the GNU Lesser General Public ++ License along with the GNU C Library; if not, see ++ . */ ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++ ++static unsigned int relocating_reallocs; ++ ++/* 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); ++ /* Clobber the old block so that a stale we_wordv pointer restored on the ++ error path reads garbage instead of the old contents, which might ++ otherwise survive intact and mask the bug. */ ++ memset (ptr, 0x5a, old); ++ free (ptr); ++ relocating_reallocs++; ++ return new; ++} ++ ++/* Verify that all words in we match the expected NULL-terminated ++ array. */ ++static void ++check_words (const wordexp_t *we, const char *const *expected) ++{ ++ 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); ++} ++ ++#define CHECK_WORDS(we, ...) \ ++ do { \ ++ const char *const expected_[] = { __VA_ARGS__, NULL }; \ ++ check_words (we, expected_); \ ++ } while (0) ++ ++/* 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); ++} ++ ++/* 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; ++ unsigned int saved_reallocs = relocating_reallocs; ++ ++ /* 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); ++ /* Verify that a relocating realloc actually happened during the failed ++ call, otherwise the pointer-stability check is vacuous. */ ++ TEST_VERIFY (relocating_reallocs > saved_reallocs); ++ TEST_COMPARE (we.we_wordc, saved_count); ++ TEST_VERIFY (we.we_wordv == saved_wordv); ++ ++ wordfree (&we); ++} ++ ++/* 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); ++} ++ ++/* 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); ++ ++ char **saved_wordv = we.we_wordv; ++ ++ TEST_COMPARE (wordexp ("world", &we, WRDE_APPEND), 0); ++ TEST_COMPARE (we.we_wordc, 2); ++ /* A successful append works on a fresh copy of the array, so the ++ caller-visible pointer must have changed. */ ++ TEST_VERIFY (we.we_wordv != saved_wordv); ++ CHECK_WORDS (&we, "hello", "world"); ++ ++ wordfree (&we); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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); ++} ++ ++/* 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; ++ unsigned int saved_reallocs = relocating_reallocs; ++ ++ /* "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_VERIFY (relocating_reallocs > saved_reallocs); ++ ++ 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); ++} ++ ++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 +diff --git a/posix/wordexp.c b/posix/wordexp.c +index f0f69ee85d..8fdc8b8caf 100644 +--- a/posix/wordexp.c ++++ b/posix/wordexp.c +@@ -48,6 +48,7 @@ + /* Undefine the following line for the production version. */ + /* #define NDEBUG 1 */ + #include ++#include + + /* Get some device information. */ + #include +@@ -2249,6 +2250,12 @@ wordexp (const char *words, wordexp_t *p + char ifs_white[4]; + wordexp_t old_word = *pwordexp; + ++ /* 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; ++ + if (flags & WRDE_REUSE) + { + /* Minimal implementation of WRDE_REUSE for now */ +@@ -2283,6 +2290,23 @@ wordexp (const char *words, wordexp_t *p + pwordexp->we_offs = 0; + } + } ++ 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); ++ if (dup == NULL) ++ return WRDE_NOSPACE; ++ memcpy (dup, pwordexp->we_wordv, num_p * sizeof *dup); ++ saved_wordv = pwordexp->we_wordv; ++ pwordexp->we_wordv = dup; ++ } + + /* Find out what the field separators are. + * There are two types: whitespace and non-whitespace. +@@ -2363,7 +2387,7 @@ wordexp (const char *words, wordexp_t *p + error = w_addword (pwordexp, NULL); + + if (error) +- return error; ++ goto do_error; + } + + break; +@@ -2381,7 +2405,7 @@ wordexp (const char *words, wordexp_t *p + error = w_addword (pwordexp, NULL); + + if (error) +- return error; ++ goto do_error; + } + + break; +@@ -2447,10 +2471,18 @@ wordexp (const char *words, wordexp_t *p + + /* There was a word separator at the end */ + if (word == NULL) /* i.e. w_newword */ +- return 0; ++ { ++ free (saved_wordv); ++ return 0; ++ } + +- /* There was no field separator at the end */ +- return w_addword (pwordexp, word); ++ /* There was no field separator at the end. The only possible error ++ from w_addword is WRDE_NOSPACE. */ ++ error = w_addword (pwordexp, word); ++ if (error != 0) ++ goto do_error; ++ free (saved_wordv); ++ return 0; + + do_error: + /* Error: +@@ -2461,11 +2493,30 @@ do_error: + free (word); + + 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; ++ } + +- 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; + }