{"id":2226367,"url":"http://patchwork.ozlabs.org/api/1.2/patches/2226367/?format=json","web_url":"http://patchwork.ozlabs.org/project/glibc/patch/20260422122514.603156-1-adhemerval.zanella@linaro.org/","project":{"id":41,"url":"http://patchwork.ozlabs.org/api/1.2/projects/41/?format=json","name":"GNU C Library","link_name":"glibc","list_id":"libc-alpha.sourceware.org","list_email":"libc-alpha@sourceware.org","web_url":"","scm_url":"","webscm_url":"","list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<20260422122514.603156-1-adhemerval.zanella@linaro.org>","list_archive_url":null,"date":"2026-04-22T12:24:51","name":"posix: Fix wordexp WRDE_APPEND to preserve state on non-NOSPACE errors (BZ 34090, CVE-2026-6368)","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"ffe2d9e12db9c648cf48b81ea629514b89306c86","submitter":{"id":66065,"url":"http://patchwork.ozlabs.org/api/1.2/people/66065/?format=json","name":"Adhemerval Zanella Netto","email":"adhemerval.zanella@linaro.org"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/glibc/patch/20260422122514.603156-1-adhemerval.zanella@linaro.org/mbox/","series":[{"id":501003,"url":"http://patchwork.ozlabs.org/api/1.2/series/501003/?format=json","web_url":"http://patchwork.ozlabs.org/project/glibc/list/?series=501003","date":"2026-04-22T12:24:51","name":"posix: Fix wordexp WRDE_APPEND to preserve state on non-NOSPACE errors (BZ 34090, CVE-2026-6368)","version":1,"mbox":"http://patchwork.ozlabs.org/series/501003/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/2226367/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2226367/checks/","tags":{},"related":[],"headers":{"Return-Path":"<libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org>","X-Original-To":["incoming@patchwork.ozlabs.org","libc-alpha@sourceware.org"],"Delivered-To":["patchwork-incoming@legolas.ozlabs.org","libc-alpha@sourceware.org"],"Authentication-Results":["legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256\n header.s=google header.b=tcn4KVw/;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=sourceware.org\n (client-ip=2620:52:6:3111::32; helo=vm01.sourceware.org;\n envelope-from=libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org;\n receiver=patchwork.ozlabs.org)","sourceware.org;\n\tdkim=pass (2048-bit key,\n unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256\n header.s=google header.b=tcn4KVw/","sourceware.org;\n dmarc=pass (p=none dis=none) header.from=linaro.org","sourceware.org; spf=pass smtp.mailfrom=linaro.org","server2.sourceware.org;\n arc=none smtp.remote-ip=2607:f8b0:4864:20::e2e"],"Received":["from vm01.sourceware.org (vm01.sourceware.org\n [IPv6:2620:52:6:3111::32])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4g0z3603Z2z1yCv\n\tfor <incoming@patchwork.ozlabs.org>; Wed, 22 Apr 2026 22:25:47 +1000 (AEST)","from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id 54BCD4BBC0DF\n\tfor <incoming@patchwork.ozlabs.org>; Wed, 22 Apr 2026 12:25:45 +0000 (GMT)","from mail-vs1-xe2e.google.com (mail-vs1-xe2e.google.com\n [IPv6:2607:f8b0:4864:20::e2e])\n by sourceware.org (Postfix) with ESMTPS id 765C54BB3B81\n for <libc-alpha@sourceware.org>; Wed, 22 Apr 2026 12:25:22 +0000 (GMT)","by mail-vs1-xe2e.google.com with SMTP id\n ada2fe7eead31-605def5b807so1613660137.3\n for <libc-alpha@sourceware.org>; Wed, 22 Apr 2026 05:25:22 -0700 (PDT)","from mandiga.. ([2804:1b3:a7c3:d5d0:3e95:69d2:d83e:ba1f])\n by smtp.gmail.com with ESMTPSA id\n ada2fe7eead31-6183b806b0asm6876239137.10.2026.04.22.05.25.17\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Wed, 22 Apr 2026 05:25:19 -0700 (PDT)"],"DKIM-Filter":["OpenDKIM Filter v2.11.0 sourceware.org 54BCD4BBC0DF","OpenDKIM Filter v2.11.0 sourceware.org 765C54BB3B81"],"DMARC-Filter":"OpenDMARC Filter v1.4.2 sourceware.org 765C54BB3B81","ARC-Filter":"OpenARC Filter v1.0.0 sourceware.org 765C54BB3B81","ARC-Seal":"i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776860722; cv=none;\n b=FLE5sjCbf1z7/q5XiWsgDoo2UOjoB6sMMcpobbB7t97yP8bz+rNz4ET3Uh2iRor99/PGOGkKoykBnp9DZQLtz6n3iypyVD5jjhDMIpUEyqi+z+Yi5ym0mp8qx/iTViDw0B0qFnLDk4M0DVtGhL+w9hFE/gtw3eNu9XJjtBl5S+4=","ARC-Message-Signature":"i=1; a=rsa-sha256; d=sourceware.org; s=key;\n t=1776860722; c=relaxed/simple;\n bh=uKN24yYkD6VIyrKcYmSFS1ia1Ydvq2ClOV5xV9zKISE=;\n h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version;\n b=K2Bo45btsVeYRJMTDReCwqkM62iEvIRS9t0Q62nDx3PZJOTF7kLdM9ZaM01pZfw+EMzhESVF4Ye8+tYW3BMac0ORjQ6Z/qEU9dBsUuK9aFi/UzF+pXbN8jzrxmCsdY6Ql3w4kAUqLvx82MurEhrWEG3UqbVN8g8NF0nshdCYL6o=","ARC-Authentication-Results":"i=1; server2.sourceware.org","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=linaro.org; s=google; t=1776860720; x=1777465520; darn=sourceware.org;\n h=content-transfer-encoding:mime-version:message-id:date:subject:cc\n :to:from:from:to:cc:subject:date:message-id:reply-to;\n bh=WqE1JRNyH97QWEhoVQfMtr82e5J+gVqJ5Za/kVtEk8A=;\n b=tcn4KVw/s8Ab5EOqbCx/Gm5t2YK2CAAiq9JdeXHMRHNp0/u9EL0BOa2m03w7rtroqv\n GgvQ1ym6QD2aDtHV+qLcdYtGFwTLZNuTmtZc4kSlAyfsnLZuGYtLzK4f+h15KKgCcKqJ\n bK6Ihe57Y992cmNgaCv+9mnevdjSBTO8dWZlRCvQIptoxghP8yTroK8x5dYuxXEr3buG\n BLcZPJ7B6XOIaL/7bDG26xf+5pNasuPBVgGqmVrXbNigeo1iN4rhIx9XTdIJOjupZrMn\n vwstBRUOUCcjoIERWQArBw1ohqhOD4AnJBWWc0Mrt+/RS7qaL/pcZYFJ/2mo/zLk6S13\n aokA==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1776860720; x=1777465520;\n h=content-transfer-encoding:mime-version:message-id:date:subject:cc\n :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date\n :message-id:reply-to;\n bh=WqE1JRNyH97QWEhoVQfMtr82e5J+gVqJ5Za/kVtEk8A=;\n b=Cicb9AklDFJlUChiveVFFvKoSQJmX1BUktPpM5iiPRowGHdECMeNIsbGZeOUKrbErO\n D8H+DfOVn8bRxKsh6AokqEmvRnoE9nrasHVOHLgYGv/qWolMm1i44S2zSmhMBujJiGh5\n Qt5IohnjnTR802JfYneliIYPVM5g397y4ct38a/I84CKNLB4r7Rn5swT1/RriwxYvOm6\n kFMxGbI/YvwlAm54+uuK8VcaNC2UTgLfYksZeqXzpElVhQd+J+sO6NsV9AWPhw371eJ1\n lGRNanld8TSz+LPYrsWc4YQPazJBr6r3B+tHELkMSP5jR5fS3HF5NOM3BGuUkSfWD6ch\n n4DA==","X-Gm-Message-State":"AOJu0YyNVd0OXkWF4sR3N2B3HgiOUUr0BNC51BCMdEQHl/rf567FxEaB\n dcgGMeDlqphI9pcWeWnjSFrvqj/VC/XLCIzkokCfxRV4Hx4zEgpH6WQztLpruUywa94qxWF7F+b\n 77KHK","X-Gm-Gg":"AeBDievodkP65Qaz5aFP3pkoJjQMiJBIOVxk5ITLjSYSTst7Cdd3H4z5ecK/Tshonqs\n 2+7UvnJLcq/F1qgI2VwNSZF2Iv4w6zp5cVDcl6XEOyQuFXWdHBbNIyfIUYCoPRQA9em/f5Atbfk\n 6rgaIdHbBU4b+K6AT+uOamha+rVSb7Hv91k/1nRCRQcxfEap4410wU8WE0QKIFlwdXjwucgTwLK\n DoPJ4vym0GgarlQrrjVR6FfLlEhL+uoP39itYyeLMWRSSVniznTBPG/y796tYXjUTchXiN93MB0\n xAQwUc9dxwaHhcrt+chF/33qlRKSuymoxl8YhvWbI8+PWdpChDuaOGHiwdWMetq2lJ8ieR0J/ad\n hoejujIL5NHpfVx89el29Mei2llBigjL4ZGcDRwzEM42yTSOeyPY0ClEa9qheK8vdrUTke1yTHD\n tHYddsUOKPr/N+L4dEG1g7lOTovK+SnMtkeYxcFRKcQKVNHQ==","X-Received":"by 2002:a05:6102:50a4:b0:602:aac7:b8bc with SMTP id\n ada2fe7eead31-616f7c5fd9cmr9568403137.30.1776860720132;\n Wed, 22 Apr 2026 05:25:20 -0700 (PDT)","From":"Adhemerval Zanella <adhemerval.zanella@linaro.org>","To":"libc-alpha@sourceware.org","Cc":"Siddhesh Poyarekar <siddhesh@gotplt.org>,\n Carlos O'Donell <carlos@redhat.com>","Subject":"[PATCH] posix: Fix wordexp WRDE_APPEND to preserve state on\n non-NOSPACE errors (BZ 34090, CVE-2026-6368)","Date":"Wed, 22 Apr 2026 09:24:51 -0300","Message-ID":"<20260422122514.603156-1-adhemerval.zanella@linaro.org>","X-Mailer":"git-send-email 2.43.0","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","X-BeenThere":"libc-alpha@sourceware.org","X-Mailman-Version":"2.1.30","Precedence":"list","List-Id":"Libc-alpha mailing list <libc-alpha.sourceware.org>","List-Unsubscribe":"<https://sourceware.org/mailman/options/libc-alpha>,\n <mailto:libc-alpha-request@sourceware.org?subject=unsubscribe>","List-Archive":"<https://sourceware.org/pipermail/libc-alpha/>","List-Post":"<mailto:libc-alpha@sourceware.org>","List-Help":"<mailto:libc-alpha-request@sourceware.org?subject=help>","List-Subscribe":"<https://sourceware.org/mailman/listinfo/libc-alpha>,\n <mailto:libc-alpha-request@sourceware.org?subject=subscribe>","Errors-To":"libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org"},"content":"The previous implementation saved a copy of the wordexp_t struct at\nentry and blindly restored it on error via (*pwordexp = old_word).\nThis is incorrect when WRDE_APPEND is set because w_addword may have\ncalled realloc on we_wordv during partial processing before the error\nwas detected.  If realloc relocated the buffer, the saved we_wordv\npointer is dangling; restoring it causes a use-after-free in the\ncaller (e.g. via wordfree), and the relocated buffer is leaked.\n\nFix this by duplicating the we_wordv pointer array at entry when\nWRDE_APPEND is set, so that all subsequent realloc calls inside\nw_addword operate on the copy.\n\nThis change also fixes a POSIX conformance issue: if the WRDE_APPEND\nflag is specified, pwordexp->we_wordc and pwordexp->we_wordv shall\nnot be modified.\n\nAlso fix two pre-existing error return paths in the '\"' and '\\'' cases\nthat returned directly from w_addword failures instead of going through\ndo_error, which would leak the saved array (and previously would also\nskip the word cleanup).\n\nA new test (tst-wordexp-append) exercises the changes:\n\n- we_wordc and we_wordv pointer preserved on WRDE_BADCHAR/WRDE_SYNTAX\n- we_wordv pointer stability when realloc relocates the buffer\n- original words remain accessible after a failed append\n- successful append still works (regression)\n- recovery: successful append after a failed one\n- repeated failures do not corrupt the state\n- error without WRDE_APPEND still cleans up properly\n- WRDE_BADCHAR on the first character (no partial w_addword)\n- WRDE_APPEND into an empty (zeroed) wordexp_t\n\nChecked on x86_64-linux-gnu and i686-linux-gnu.\n---\n posix/Makefile             |   1 +\n posix/tst-wordexp-append.c | 307 +++++++++++++++++++++++++++++++++++++\n posix/wordexp.c            |  64 +++++++-\n 3 files changed, 364 insertions(+), 8 deletions(-)\n create mode 100644 posix/tst-wordexp-append.c","diff":"diff --git a/posix/Makefile b/posix/Makefile\nindex a5e5162c61..1ee73b0b65 100644\n--- a/posix/Makefile\n+++ b/posix/Makefile\n@@ -329,6 +329,7 @@ tests := \\\n   tst-wait3 \\\n   tst-wait4 \\\n   tst-waitid \\\n+  tst-wordexp-append \\\n   tst-wordexp-nocmd \\\n   tst-wordexp-reuse \\\n   tstgetopt \\\ndiff --git a/posix/tst-wordexp-append.c b/posix/tst-wordexp-append.c\nnew file mode 100644\nindex 0000000000..6253520c57\n--- /dev/null\n+++ b/posix/tst-wordexp-append.c\n@@ -0,0 +1,307 @@\n+/* Test for wordexp with WRDE_APPEND flag.\n+   Copyright (C) 2026 Free Software Foundation, Inc.\n+   This file is part of the GNU C Library.\n+\n+   The GNU C Library is free software; you can redistribute it and/or\n+   modify it under the terms of the GNU Lesser General Public\n+   License as published by the Free Software Foundation; either\n+   version 2.1 of the License, or (at your option) any later version.\n+\n+   The GNU C Library is distributed in the hope that it will be useful,\n+   but WITHOUT ANY WARRANTY; without even the implied warranty of\n+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n+   Lesser General Public License for more details.\n+\n+   You should have received a copy of the GNU Lesser General Public\n+   License along with the GNU C Library; if not, see\n+   <https://www.gnu.org/licenses/>.  */\n+\n+#include <wordexp.h>\n+#include <string.h>\n+#include <stdio.h>\n+#include <stdlib.h>\n+\n+#include <support/check.h>\n+#include <support/support.h>\n+\n+/* Attempt to force realloc to relocate the we_wordv buffer by placing an\n+   allocation right after it.  Returns a pointer that must be freed after\n+   the test.  */\n+static void *\n+place_blocker (void)\n+{\n+  void *p = xmalloc (0x1000);\n+  /* Write to it so the compiler cannot optimize it away and the allocator\n+     actually commits the pages.  */\n+  memset (p, 0x41, 0x1000);\n+  return p;\n+}\n+\n+/* Verify that all words in we match the expected NULL-terminated\n+   array.  */\n+static void\n+check_words (const wordexp_t *we, const char *const *expected, int line)\n+{\n+  size_t i;\n+  for (i = 0; expected[i] != NULL; i++)\n+    {\n+      TEST_VERIFY (i < we->we_wordc);\n+      TEST_COMPARE_STRING (we->we_wordv[we->we_offs + i], expected[i]);\n+    }\n+  TEST_COMPARE (we->we_wordc, i);\n+}\n+\n+#define CHECK_WORDS(we, ...) \\\n+  do {\t\t\t\t\t\t\t\t\\\n+    const char *const expected_[] = { __VA_ARGS__, NULL };\t\\\n+    check_words (we, expected_, __LINE__);\t\t\t\\\n+  } while (0)\n+\n+/* Test 1: WRDE_APPEND + WRDE_BADCHAR preserves we_wordc.  */\n+static void\n+test_append_badchar_preserves_count (void)\n+{\n+  printf (\"info: test_append_badchar_preserves_count\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"one two three\", &we, 0), 0);\n+  TEST_COMPARE (we.we_wordc, 3);\n+\n+  size_t saved_count = we.we_wordc;\n+\n+  /* ')' triggers WRDE_BADCHAR and  \"extra\" would be a new word if the\n+     expansion succeeded, exercising the w_addword path before the error\n+     is detected.  */\n+  TEST_COMPARE (wordexp (\"extra )\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (we.we_wordc, saved_count);\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 2: WRDE_APPEND + WRDE_BADCHAR preserves the we_wordv pointer even\n+   when internal realloc would move the buffer.  */\n+static void\n+test_append_badchar_preserves_pointer (void)\n+{\n+  printf (\"info: test_append_badchar_preserves_pointer\\n\");\n+  wordexp_t we = { 0 };\n+\n+  /* Use many words so that the initial we_wordv allocation is\n+     non-trivial and a later realloc is more likely to move it.  */\n+  TEST_COMPARE (wordexp (\"a b c d e f g h\", &we, 0), 0);\n+  TEST_COMPARE (we.we_wordc, 8);\n+\n+  char **saved_wordv = we.we_wordv;\n+  size_t saved_count = we.we_wordc;\n+\n+  /* Place an allocation right after to make realloc move the buffer.  */\n+  void *blocker = place_blocker ();\n+\n+  TEST_COMPARE (wordexp (\"append )\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (we.we_wordc, saved_count);\n+  TEST_VERIFY (we.we_wordv == saved_wordv);\n+\n+  free (blocker);\n+  wordfree (&we);\n+}\n+\n+/* Test 3: After a failed WRDE_APPEND the original words are still accessible\n+   and correct.  */\n+static void\n+test_append_badchar_words_intact (void)\n+{\n+  printf (\"info: test_append_badchar_words_intact\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"alpha beta gamma\", &we, 0), 0);\n+  CHECK_WORDS (&we, \"alpha\", \"beta\", \"gamma\");\n+\n+  void *blocker = place_blocker ();\n+  TEST_COMPARE (wordexp (\"delta )\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  free (blocker);\n+\n+  /* Words must still be intact.  */\n+  CHECK_WORDS (&we, \"alpha\", \"beta\", \"gamma\");\n+  /* The NULL terminator must still be present.  */\n+  TEST_VERIFY (we.we_wordv[we.we_offs + we.we_wordc] == NULL);\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 4: Successful WRDE_APPEND still works (regression test).  */\n+static void\n+test_append_success (void)\n+{\n+  printf (\"info: test_append_success\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"hello\", &we, 0), 0);\n+  TEST_COMPARE (we.we_wordc, 1);\n+\n+  TEST_COMPARE (wordexp (\"world\", &we, WRDE_APPEND), 0);\n+  TEST_COMPARE (we.we_wordc, 2);\n+  CHECK_WORDS (&we, \"hello\", \"world\");\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 5: Successful append after a failed append — the implementation must\n+   recover and allow further use of the wordexp_t.  */\n+static void\n+test_append_success_after_failure (void)\n+{\n+  printf (\"info: test_append_success_after_failure\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"first\", &we, 0), 0);\n+  CHECK_WORDS (&we, \"first\");\n+\n+  void *blocker = place_blocker ();\n+  TEST_COMPARE (wordexp (\"bad |\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  free (blocker);\n+\n+  /* State must be exactly as before the failed call.  */\n+  CHECK_WORDS (&we, \"first\");\n+\n+  /* A subsequent successful append must work.  */\n+  TEST_COMPARE (wordexp (\"second third\", &we, WRDE_APPEND), 0);\n+  CHECK_WORDS (&we, \"first\", \"second\", \"third\");\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 6: Multiple consecutive failed appends do not corrupt state.  */\n+static void\n+test_append_multiple_failures (void)\n+{\n+  printf (\"info: test_append_multiple_failures\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"keep this\", &we, 0), 0);\n+  CHECK_WORDS (&we, \"keep\", \"this\");\n+\n+  size_t saved_count = we.we_wordc;\n+  char **saved_wordv = we.we_wordv;\n+\n+  void *blocker = place_blocker ();\n+\n+  /* Each of these bad characters must leave the state unchanged.  */\n+  TEST_COMPARE (wordexp (\"x )\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (wordexp (\"x |\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (wordexp (\"x ;\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (wordexp (\"x &\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (wordexp (\"x <\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (wordexp (\"x >\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+\n+  free (blocker);\n+\n+  TEST_COMPARE (we.we_wordc, saved_count);\n+  TEST_VERIFY (we.we_wordv == saved_wordv);\n+  CHECK_WORDS (&we, \"keep\", \"this\");\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 7: WRDE_APPEND with WRDE_SYNTAX error (unterminated quote) also\n+   preserves state.  */\n+static void\n+test_append_syntax_error (void)\n+{\n+  printf (\"info: test_append_syntax_error\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"original\", &we, 0), 0);\n+  CHECK_WORDS (&we, \"original\");\n+\n+  char **saved_wordv = we.we_wordv;\n+  size_t saved_count = we.we_wordc;\n+\n+  void *blocker = place_blocker ();\n+  /* Unterminated double quote triggers WRDE_SYNTAX.  */\n+  TEST_COMPARE (wordexp (\"\\\"unterminated\", &we, WRDE_APPEND), WRDE_SYNTAX);\n+  free (blocker);\n+\n+  TEST_COMPARE (we.we_wordc, saved_count);\n+  TEST_VERIFY (we.we_wordv == saved_wordv);\n+  CHECK_WORDS (&we, \"original\");\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 8: Error without WRDE_APPEND still works (regression test for the\n+   non-APPEND code path in do_error).  */\n+static void\n+test_no_append_error (void)\n+{\n+  printf (\"info: test_no_append_error\\n\");\n+  wordexp_t we = { 0 };\n+\n+  /* Simple failure without WRDE_APPEND.  */\n+  TEST_COMPARE (wordexp (\"bad |\", &we, 0), WRDE_BADCHAR);\n+\n+  /* After failure without WRDE_APPEND the struct should be safe to\n+     reuse — start fresh.  */\n+  TEST_COMPARE (wordexp (\"ok\", &we, 0), 0);\n+  CHECK_WORDS (&we, \"ok\");\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 9: WRDE_BADCHAR on the very first character (no partial words added\n+   before the error).  */\n+static void\n+test_append_badchar_immediate (void)\n+{\n+  printf (\"info: test_append_badchar_immediate\\n\");\n+  wordexp_t we = { 0 };\n+\n+  TEST_COMPARE (wordexp (\"hello world\", &we, 0), 0);\n+  CHECK_WORDS (&we, \"hello\", \"world\");\n+\n+  char **saved_wordv = we.we_wordv;\n+  size_t saved_count = we.we_wordc;\n+\n+  /* The bad character is the very first byte — no w_addword call happens\n+     before the error.  */\n+  TEST_COMPARE (wordexp (\"|\", &we, WRDE_APPEND), WRDE_BADCHAR);\n+  TEST_COMPARE (we.we_wordc, saved_count);\n+  TEST_VERIFY (we.we_wordv == saved_wordv);\n+\n+  wordfree (&we);\n+}\n+\n+/* Test 10: WRDE_APPEND into an empty wordexp_t (initial call uses WRDE_APPEND\n+   with a zeroed struct — unusual but allowed).  */\n+static void\n+test_append_into_empty (void)\n+{\n+  printf (\"info: test_append_into_empty\\n\");\n+  wordexp_t we = { 0 };\n+\n+  /* First call with WRDE_APPEND on a zeroed struct.  The implementation\n+     must handle we_wordv == NULL gracefully.  */\n+  TEST_COMPARE (wordexp (\"solo\", &we, WRDE_APPEND), 0);\n+  TEST_COMPARE (we.we_wordc, 1);\n+  CHECK_WORDS (&we, \"solo\");\n+\n+  wordfree (&we);\n+}\n+\n+static int\n+do_test (void)\n+{\n+  test_append_badchar_preserves_count ();\n+  test_append_badchar_preserves_pointer ();\n+  test_append_badchar_words_intact ();\n+  test_append_success ();\n+  test_append_success_after_failure ();\n+  test_append_multiple_failures ();\n+  test_append_syntax_error ();\n+  test_no_append_error ();\n+  test_append_badchar_immediate ();\n+  test_append_into_empty ();\n+\n+  return 0;\n+}\n+\n+#include <support/test-driver.c>\ndiff --git a/posix/wordexp.c b/posix/wordexp.c\nindex 4a8541add4..119ce05d9c 100644\n--- a/posix/wordexp.c\n+++ b/posix/wordexp.c\n@@ -35,6 +35,7 @@\n #include <scratch_buffer.h>\n #include <_itoa.h>\n #include <assert.h>\n+#include <intprops.h>\n \n /*\n  * This is a recursive-descent-style word expansion routine.\n@@ -2212,6 +2213,12 @@ wordexp (const char *words, wordexp_t *pwordexp, int flags)\n   char ifs_white[4];\n   wordexp_t old_word = *pwordexp;\n \n+  /* When WRDE_APPEND is set we work on a copy of the we_wordv array so that\n+     the caller's original pointer is never invalidated by realloc inside\n+     w_addword.  The saved_wordv keeps the original; on success we free it,\n+     on non-NOSPACE error we free the working copy and restore the original.  */\n+  char **saved_wordv = NULL;\n+\n   if (flags & WRDE_REUSE)\n     {\n       /* Minimal implementation of WRDE_REUSE for now */\n@@ -2246,6 +2253,23 @@ wordexp (const char *words, wordexp_t *pwordexp, int flags)\n \t  pwordexp->we_offs = 0;\n \t}\n     }\n+  else if (pwordexp->we_wordv != NULL)\n+    {\n+      /* WRDE_APPEND with an existing word list: duplicate the array so that\n+\t realloc during parsing does not invalidate the caller's pointer.  The\n+\t strings themselves are shared.  */\n+      size_t num_p;\n+      char **dup;\n+      if (INT_ADD_WRAPV (pwordexp->we_offs, pwordexp->we_wordc, &num_p)\n+\t  || INT_ADD_WRAPV (num_p, 1, &num_p))\n+\treturn WRDE_NOSPACE;\n+      dup = __libc_reallocarray (NULL, num_p, sizeof *dup);\n+      if (dup == NULL)\n+\treturn WRDE_NOSPACE;\n+      memcpy (dup, pwordexp->we_wordv, num_p * sizeof *dup);\n+      saved_wordv = pwordexp->we_wordv;\n+      pwordexp->we_wordv = dup;\n+    }\n \n   /* Find out what the field separators are.\n    * There are two types: whitespace and non-whitespace.\n@@ -2326,7 +2350,7 @@ wordexp (const char *words, wordexp_t *pwordexp, int flags)\n \t    error = w_addword (pwordexp, NULL);\n \n \t    if (error)\n-\t      return error;\n+\t      goto do_error;\n \t  }\n \n \tbreak;\n@@ -2344,7 +2368,7 @@ wordexp (const char *words, wordexp_t *pwordexp, int flags)\n \t    error = w_addword (pwordexp, NULL);\n \n \t    if (error)\n-\t      return error;\n+\t      goto do_error;\n \t  }\n \n \tbreak;\n@@ -2410,10 +2434,15 @@ wordexp (const char *words, wordexp_t *pwordexp, int flags)\n \n   /* There was a word separator at the end */\n   if (word == NULL) /* i.e. w_newword */\n-    return 0;\n+    {\n+      free (saved_wordv);\n+      return 0;\n+    }\n \n   /* There was no field separator at the end */\n-  return w_addword (pwordexp, word);\n+  error = w_addword (pwordexp, word);\n+  free (saved_wordv);\n+  return error;\n \n do_error:\n   /* Error:\n@@ -2424,11 +2453,30 @@ do_error:\n   free (word);\n \n   if (error == WRDE_NOSPACE)\n-    return WRDE_NOSPACE;\n+    {\n+      /* we_wordc and we_wordv are updated to reflect any words that were\n+\t successfully expanded.  The old array is obsolete.  */\n+      free (saved_wordv);\n+      return WRDE_NOSPACE;\n+    }\n \n-  if ((flags & WRDE_APPEND) == 0)\n-    wordfree (pwordexp);\n+  if (flags & WRDE_APPEND)\n+    {\n+      /* POSIX 2024 states that for in other error cases, if the WRDE_APPEND\n+\t flag was specified, we_wordc and we_wordv shall not be modified.\n+\n+\t Free strings appended during this call, discard the working copy of\n+\t we_wordv, and restore the caller's original pointer.  */\n+      while (pwordexp->we_wordc > old_word.we_wordc)\n+\tfree (pwordexp->we_wordv[pwordexp->we_offs + --pwordexp->we_wordc]);\n+      free (pwordexp->we_wordv);\n+      pwordexp->we_wordv = saved_wordv;\n+    }\n+  else\n+    {\n+      wordfree (pwordexp);\n+      *pwordexp = old_word;\n+    }\n \n-  *pwordexp = old_word;\n   return error;\n }\n","prefixes":[]}