Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/1.2/patches/2226367/?format=api
{ "id": 2226367, "url": "http://patchwork.ozlabs.org/api/1.2/patches/2226367/?format=api", "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=api", "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=api", "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=api", "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": [] }