posix: Fix stack overflow in wordexp tilde expansion (BZ 34091, CVE-2026-6791)
Checks
| Context |
Check |
Description |
| redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
| redhat-pt-bot/TryBot-32bit |
success
|
Build for i686
|
Commit Message
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
Comments
* Adhemerval Zanella:
> + /* 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';
>
> + struct passwd pwd, *tpwd;
> + int result;
> + while ((result = __getpwnam_r (user,
> + &pwd,
> + tmpbuf.data + userlen + 1,
> + tmpbuf.length - userlen - 1,
> + &tpwd)
While this is technically correct, this looks like a bit of overkill.
Maybe just use __strndup? There is no reason to optimize this with an
on-stack allocation.
Thanks,
Florian
On 22/04/26 09:46, Florian Weimer wrote:
> * Adhemerval Zanella:
>
>> + /* 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';
>>
>> + struct passwd pwd, *tpwd;
>> + int result;
>> + while ((result = __getpwnam_r (user,
>> + &pwd,
>> + tmpbuf.data + userlen + 1,
>> + tmpbuf.length - userlen - 1,
>> + &tpwd)
>
> While this is technically correct, this looks like a bit of overkill.
> Maybe just use __strndup? There is no reason to optimize this with an
> on-stack allocation.
It keeps the same performance characteristic and is one less buffer to manage
(cleanup is done exclusive by scratch_buffer_free).
* Adhemerval Zanella Netto:
> On 22/04/26 09:46, Florian Weimer wrote:
>> * Adhemerval Zanella:
>>
>>> + /* 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';
>>>
>>> + struct passwd pwd, *tpwd;
>>> + int result;
>>> + while ((result = __getpwnam_r (user,
>>> + &pwd,
>>> + tmpbuf.data + userlen + 1,
>>> + tmpbuf.length - userlen - 1,
>>> + &tpwd)
>>
>> While this is technically correct, this looks like a bit of overkill.
>> Maybe just use __strndup? There is no reason to optimize this with an
>> on-stack allocation.
>
> It keeps the same performance characteristic and is one less buffer to manage
> (cleanup is done exclusive by scratch_buffer_free).
The cost is that it turns a standard _r retry loop into a non-standard
one.
I don't have a strong opinion about this. I hope to get rid of internal
use of those _r functions eventually, at which point we can switch this
to strndup.
Thanks,
Florian
On 22/04/26 10:07, Florian Weimer wrote:
> * Adhemerval Zanella Netto:
>
>> On 22/04/26 09:46, Florian Weimer wrote:
>>> * Adhemerval Zanella:
>>>
>>>> + /* 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';
>>>>
>>>> + struct passwd pwd, *tpwd;
>>>> + int result;
>>>> + while ((result = __getpwnam_r (user,
>>>> + &pwd,
>>>> + tmpbuf.data + userlen + 1,
>>>> + tmpbuf.length - userlen - 1,
>>>> + &tpwd)
>>>
>>> While this is technically correct, this looks like a bit of overkill.
>>> Maybe just use __strndup? There is no reason to optimize this with an
>>> on-stack allocation.
>>
>> It keeps the same performance characteristic and is one less buffer to manage
>> (cleanup is done exclusive by scratch_buffer_free).
>
> The cost is that it turns a standard _r retry loop into a non-standard
> one.
>
The extra cost is from scratch_buffer_grow_preserve, but since this should
be not the usual path I don't think this is a problem. The wordexp
already uses a lot of dynamic allocation, I think we should try to avoid
one extra if possible.
> I don't have a strong opinion about this. I hope to get rid of internal
> use of those _r functions eventually, at which point we can switch this
> to strndup.
>
> Thanks,
> Florian
>
* Adhemerval Zanella Netto:
> On 22/04/26 10:07, Florian Weimer wrote:
>> * Adhemerval Zanella Netto:
>>
>>> On 22/04/26 09:46, Florian Weimer wrote:
>>>> * Adhemerval Zanella:
>>>>
>>>>> + /* 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';
>>>>>
>>>>> + struct passwd pwd, *tpwd;
>>>>> + int result;
>>>>> + while ((result = __getpwnam_r (user,
>>>>> + &pwd,
>>>>> + tmpbuf.data + userlen + 1,
>>>>> + tmpbuf.length - userlen - 1,
>>>>> + &tpwd)
>>>>
>>>> While this is technically correct, this looks like a bit of overkill.
>>>> Maybe just use __strndup? There is no reason to optimize this with an
>>>> on-stack allocation.
>>>
>>> It keeps the same performance characteristic and is one less buffer to manage
>>> (cleanup is done exclusive by scratch_buffer_free).
>>
>> The cost is that it turns a standard _r retry loop into a non-standard
>> one.
>>
>
> The extra cost is from scratch_buffer_grow_preserve, but since this should
> be not the usual path I don't think this is a problem. The wordexp
> already uses a lot of dynamic allocation, I think we should try to avoid
> one extra if possible.
Not an objection from me then. We can change it to something else once
the need arises.
Thanks,
Florian
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);
@@ -359,6 +359,7 @@ tests-internal := \
tests-container := \
bug-ga2 \
tst-vfork3 \
+ tst-wordexp-tilde \
# tests-container
tests-time64 := \
new file mode 100644
@@ -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>
new file mode 100644
@@ -0,0 +1 @@
+root:x:0:
new file mode 100644
@@ -0,0 +1,3 @@
+passwd: files
+group: files
+shadow: files
new file mode 100644
@@ -0,0 +1 @@
+root:x:0:0:root:/root:/bin/sh
@@ -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);