[PATCH] posix: Fix stack overflow in wordexp tilde expansion (BZ 34091, CVE-2026-6791)
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Fri Jun 5 18:33:01 GMT 2026
Ping.
On 18/05/26 13:32, Adhemerval Zanella Netto wrote:
> Ping.
>
> On 22/04/26 09:35, Adhemerval Zanella wrote:
>> The parse_tilde function previously used strndupa to allocate memory
>> for the parsed username on the stack, and since the input is
>> user-defined, this can lead to a stack overflow.
>>
>> This patch fixes the issue by replacing strndupa with scratch_buffer,
>> by reusing the buffer used in the __getpwnam_r call.
>>
>> The new “tst-wordexp-tilde.c” test is a test-container to avoid using
>> system-defined NSS modules.
>>
>> Checked on x86_64-linux-gnu and i686-linux-gnu.
>> ---
>> posix/Makefile | 1 +
>> posix/tst-wordexp-tilde.c | 244 ++++++++++++++++++
>> posix/tst-wordexp-tilde.root/etc/group | 1 +
>> .../tst-wordexp-tilde.root/etc/nsswitch.conf | 3 +
>> posix/tst-wordexp-tilde.root/etc/passwd | 1 +
>> posix/wordexp.c | 28 +-
>> 6 files changed, 270 insertions(+), 8 deletions(-)
>> create mode 100644 posix/tst-wordexp-tilde.c
>> create mode 100644 posix/tst-wordexp-tilde.root/etc/group
>> create mode 100644 posix/tst-wordexp-tilde.root/etc/nsswitch.conf
>> create mode 100644 posix/tst-wordexp-tilde.root/etc/passwd
>>
>> diff --git a/posix/Makefile b/posix/Makefile
>> index a5e5162c61..b2de242321 100644
>> --- a/posix/Makefile
>> +++ b/posix/Makefile
>> @@ -359,6 +359,7 @@ tests-internal := \
>> tests-container := \
>> bug-ga2 \
>> tst-vfork3 \
>> + tst-wordexp-tilde \
>> # tests-container
>>
>> tests-time64 := \
>> diff --git a/posix/tst-wordexp-tilde.c b/posix/tst-wordexp-tilde.c
>> new file mode 100644
>> index 0000000000..35f200a0b7
>> --- /dev/null
>> +++ b/posix/tst-wordexp-tilde.c
>> @@ -0,0 +1,244 @@
>> +/* Test wordexp tilde expansion with large usernames (BZ #XXXXX).
>> + 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
>> + <https://www.gnu.org/licenses/>. */
>> +
>> +#include <pwd.h>
>> +#include <stdio.h>
>> +#include <string.h>
>> +#include <wordexp.h>
>> +#include <stdlib.h>
>> +#include <sys/resource.h>
>> +
>> +#include <support/check.h>
>> +#include <support/support.h>
>> +#include <support/xunistd.h>
>> +#include <support/namespace.h>
>> +
>> +typedef void (*func_callback_t)(void);
>> +
>> +static void
>> +subprocess_small_stack (void *closure)
>> +{
>> + struct rlimit rl;
>> + TEST_COMPARE (getrlimit (RLIMIT_STACK, &rl), 0);
>> + rl.rlim_cur = 512 * 1024;
>> + TEST_COMPARE (setrlimit (RLIMIT_STACK, &rl), 0);
>> +
>> + func_callback_t func_test = closure;
>> + func_test ();
>> +}
>> +
>> +/* Build a string "~<padding>/tail" where <padding> is LEN bytes of the
>> + character CH. The caller must free the result. */
>> +static char *
>> +make_tilde_input (char ch, size_t len, const char *tail)
>> +{
>> + /* ~ + len + / + tail + \0 */
>> + size_t taillen = tail != NULL ? strlen (tail) : 0;
>> + size_t total = 1 + len + 1 + taillen + 1;
>> + char *buf = xmalloc (total);
>> + buf[0] = '~';
>> + memset (buf + 1, ch, len);
>> + buf[1 + len] = '/';
>> + if (tail != NULL)
>> + memcpy (buf + 1 + len + 1, tail, taillen);
>> + buf[total - 1] = '\0';
>> + return buf;
>> +}
>> +
>> +/* Test 1: A very long username must not crash. The username will not match
>> + any real user, so wordexp returns ~<long>/rest. */
>> +static void
>> +test_long_username (void)
>> +{
>> + printf ("info: test_long_username_no_crash\n");
>> +
>> + static const char REST[] = "rest";
>> +
>> + /* 1 MiB username — well beyond any reasonable stack frame. */
>> + const size_t long_len = 1024 * 1024;
>> + char *input = make_tilde_input ('A', long_len, REST);
>> +
>> + wordexp_t we = { 0 };
>> + int ret = wordexp (input, &we, 0);
>> + /* The (non-existent) username is invalid, so wordexp falls back to
>> + literal output: ~AAA…/rest. */
>> + TEST_COMPARE (ret, 0);
>> + TEST_COMPARE (we.we_wordc, 1);
>> +
>> + /* Verify prefix: '~' followed by long_len 'A's. */
>> + const char *result = we.we_wordv[0];
>> + TEST_COMPARE (result[0], '~');
>> + TEST_COMPARE (strlen (result),
>> + 1 /* ~ */ + long_len + sizeof (REST));
>> + for (size_t j = 1; j <= long_len; j++)
>> + if (result[j] != 'A')
>> + {
>> + printf (" mismatch at position %zu: expected 'A', got '%c'\n",
>> + j, result[j]);
>> + support_record_failure ();
>> + break;
>> + }
>> + /* Verify the tail after the username. */
>> + TEST_COMPARE_STRING (result + 1 + long_len, "/rest");
>> +
>> + wordfree (&we);
>> + free (input);
>> +}
>> +
>> +/* Test 2: A username that just exceeds the default scratch_buffer inline
>> + size (1024 bytes) exercises the scratch_buffer_set_array_size growth path
>> + without being excessively large. */
>> +static void
>> +test_scratch_buffer_growth (void)
>> +{
>> + printf ("info: test_scratch_buffer_growth\n");
>> +
>> + const size_t len = 2048;
>> + char *input = make_tilde_input ('x', len, NULL);
>> +
>> + wordexp_t we = { 0 };
>> + int ret = wordexp (input, &we, 0);
>> + TEST_COMPARE (ret, 0);
>> + TEST_COMPARE (we.we_wordc, 1);
>> +
>> + /* ~xxx…/ — the trailing slash makes a separate empty component, but
>> + wordexp merges it into the single token ~xxx…/. */
>> + const char *result = we.we_wordv[0];
>> + TEST_COMPARE (result[0], '~');
>> + for (size_t j = 1; j <= len; j++)
>> + if (result[j] != 'x')
>> + {
>> + printf (" mismatch at position %zu\n", j);
>> + support_record_failure ();
>> + break;
>> + }
>> + TEST_COMPARE (result[1 + len], '/');
>> +
>> + wordfree (&we);
>> + free (input);
>> +}
>> +
>> +/* Test 3: ~root still resolves to the correct home directory through the
>> + __getpwnam_r path. */
>> +static void
>> +test_known_user (void)
>> +{
>> + printf ("info: test_known_user\n");
>> +
>> + /* Look up root's home directory for comparison. */
>> + struct passwd *pw = getpwnam ("root");
>> + if (pw == NULL || pw->pw_dir == NULL)
>> + {
>> + printf (" SKIP: cannot look up root\n");
>> + return;
>> + }
>> +
>> + char *expected = xasprintf ("%s/file", pw->pw_dir);
>> +
>> + wordexp_t we = { 0 };
>> + TEST_COMPARE (wordexp ("~root/file", &we, 0), 0);
>> + TEST_COMPARE (we.we_wordc, 1);
>> + TEST_COMPARE_STRING (we.we_wordv[0], expected);
>> +
>> + wordfree (&we);
>> + free (expected);
>> +}
>> +
>> +/* Test 4: Bare tilde expands to $HOME. */
>> +static void
>> +test_bare_tilde (void)
>> +{
>> + printf ("info: test_bare_tilde\n");
>> +
>> + const char *home = getenv ("HOME");
>> + if (home == NULL)
>> + {
>> + printf (" SKIP: HOME is not set\n");
>> + return;
>> + }
>> +
>> + wordexp_t we = { 0 };
>> + TEST_COMPARE (wordexp ("~", &we, 0), 0);
>> + TEST_COMPARE (we.we_wordc, 1);
>> + TEST_COMPARE_STRING (we.we_wordv[0], home);
>> +
>> + wordfree (&we);
>> +}
>> +
>> +/* Test 5: Short non-existent username falls back to literal ~username output,
>> + exercising the invalid-login-name path. */
>> +static void
>> +test_unknown_user (void)
>> +{
>> + printf ("info: test_unknown_user\n");
>> +
>> + /* Pick a username that is extremely unlikely to exist. */
>> + wordexp_t we = { 0 };
>> + TEST_COMPARE (wordexp ("~no_such_user_xyzzy42", &we, 0), 0);
>> + TEST_COMPARE (we.we_wordc, 1);
>> + TEST_COMPARE_STRING (we.we_wordv[0], "~no_such_user_xyzzy42");
>> +
>> + wordfree (&we);
>> +}
>> +
>> +/* Test 6: Tilde with username and WRDE_APPEND — exercises parse_tilde's
>> + interaction with the WRDE_APPEND word list. */
>> +static void
>> +test_tilde_with_append (void)
>> +{
>> + printf ("info: test_tilde_with_append\n");
>> +
>> + const char *home = getenv ("HOME");
>> + if (home == NULL)
>> + {
>> + printf (" SKIP: HOME is not set\n");
>> + return;
>> + }
>> +
>> + wordexp_t we = { 0 };
>> + TEST_COMPARE (wordexp ("first", &we, 0), 0);
>> +
>> + TEST_COMPARE (wordexp ("~/path", &we, WRDE_APPEND), 0);
>> + TEST_COMPARE (we.we_wordc, 2);
>> + TEST_COMPARE_STRING (we.we_wordv[0], "first");
>> +
>> + char *expected = xasprintf ("%s/path", home);
>> + TEST_COMPARE_STRING (we.we_wordv[1], expected);
>> +
>> + wordfree (&we);
>> + free (expected);
>> +}
>> +
>> +static int
>> +do_test (void)
>> +{
>> + test_known_user ();
>> + test_bare_tilde ();
>> + test_unknown_user ();
>> + test_tilde_with_append ();
>> +
>> + support_isolate_in_subprocess (subprocess_small_stack,
>> + test_long_username);
>> +
>> + support_isolate_in_subprocess (subprocess_small_stack,
>> + test_scratch_buffer_growth);
>> +
>> + return 0;
>> +}
>> +
>> +#include <support/test-driver.c>
>> diff --git a/posix/tst-wordexp-tilde.root/etc/group b/posix/tst-wordexp-tilde.root/etc/group
>> new file mode 100644
>> index 0000000000..1dbf9013ee
>> --- /dev/null
>> +++ b/posix/tst-wordexp-tilde.root/etc/group
>> @@ -0,0 +1 @@
>> +root:x:0:
>> diff --git a/posix/tst-wordexp-tilde.root/etc/nsswitch.conf b/posix/tst-wordexp-tilde.root/etc/nsswitch.conf
>> new file mode 100644
>> index 0000000000..098a8d5938
>> --- /dev/null
>> +++ b/posix/tst-wordexp-tilde.root/etc/nsswitch.conf
>> @@ -0,0 +1,3 @@
>> +passwd: files
>> +group: files
>> +shadow: files
>> diff --git a/posix/tst-wordexp-tilde.root/etc/passwd b/posix/tst-wordexp-tilde.root/etc/passwd
>> new file mode 100644
>> index 0000000000..eb85a552ad
>> --- /dev/null
>> +++ b/posix/tst-wordexp-tilde.root/etc/passwd
>> @@ -0,0 +1 @@
>> +root:x:0:0:root:/root:/bin/sh
>> diff --git a/posix/wordexp.c b/posix/wordexp.c
>> index 4a8541add4..a6b9391906 100644
>> --- a/posix/wordexp.c
>> +++ b/posix/wordexp.c
>> @@ -335,17 +335,29 @@ parse_tilde (char **word, size_t *word_length, size_t *max_length,
>> else
>> {
>> /* Look up user name in database to get home directory */
>> - char *user = strndupa (&words[1 + *offset], i - (1 + *offset));
>> - struct passwd pwd, *tpwd;
>> - int result;
>> + size_t userlen = i - (1 + *offset);
>> + /* tmpbuf contains both the user and the __getpwnam_r working area. */
>> struct scratch_buffer tmpbuf;
>> scratch_buffer_init (&tmpbuf);
>> + if (!scratch_buffer_set_array_size (&tmpbuf, userlen + 1, 1))
>> + return WRDE_NOSPACE;
>> + char *user = tmpbuf.data;
>> + memcpy (user, &words[1 + *offset], userlen);
>> + user[userlen] = '\0';
>>
>> - while ((result = __getpwnam_r (user, &pwd, tmpbuf.data, tmpbuf.length,
>> - &tpwd)) != 0
>> - && errno == ERANGE)
>> - if (!scratch_buffer_grow (&tmpbuf))
>> - return WRDE_NOSPACE;
>> + struct passwd pwd, *tpwd;
>> + int result;
>> + while ((result = __getpwnam_r (user,
>> + &pwd,
>> + tmpbuf.data + userlen + 1,
>> + tmpbuf.length - userlen - 1,
>> + &tpwd)
>> + != 0))
>> + {
>> + if (!scratch_buffer_grow_preserve (&tmpbuf))
>> + return WRDE_NOSPACE;
>> + user = tmpbuf.data;
>> + }
>>
>> if (result == 0 && tpwd != NULL && pwd.pw_dir)
>> *word = w_addstr (*word, word_length, max_length, pwd.pw_dir);
>
More information about the Libc-alpha
mailing list