CVE-2026-6791: Stack overflow in wordexp tilde expansion (RHEL-238489)
Resolves: RHEL-238489
This commit is contained in:
parent
f2ef13fb43
commit
02c8aa5efe
349
glibc-RHEL-238489.patch
Normal file
349
glibc-RHEL-238489.patch
Normal file
@ -0,0 +1,349 @@
|
||||
commit 07c24f35392b727e6100d33edfdf811a6c68c218
|
||||
Author: Adhemerval Zanella <adhemerval.zanella@linaro.org>
|
||||
Date: Tue Apr 14 10:50:37 2026 -0300
|
||||
|
||||
posix: Fix stack overflow in wordexp tilde expansion (BZ 34091, CVE-2026-6791)
|
||||
|
||||
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.
|
||||
|
||||
Conflicts:
|
||||
posix/Makefile
|
||||
Adjusted for missing tests-container target.
|
||||
|
||||
diff --git a/posix/Makefile b/posix/Makefile
|
||||
index 0fa532396f..d9f897faa1 100644
|
||||
--- a/posix/Makefile
|
||||
+++ b/posix/Makefile
|
||||
@@ -101,6 +101,9 @@ tests := test-errno tstgetopt testfnm r
|
||||
tests-internal := bug-regex5 bug-regex20 bug-regex33 \
|
||||
tst-rfc3484 tst-rfc3484-2 tst-rfc3484-3 \
|
||||
tst-glob_lstat_compat tst-spawn4-compat
|
||||
+tests-container := \
|
||||
+ tst-wordexp-tilde \
|
||||
+ # tests-caontainer
|
||||
xtests := bug-ga2 tst-getaddrinfo4 tst-getaddrinfo5
|
||||
ifeq (yes,$(build-shared))
|
||||
test-srcs := globtest
|
||||
diff --git a/posix/tst-wordexp-tilde.c b/posix/tst-wordexp-tilde.c
|
||||
new file mode 100644
|
||||
index 0000000000..1661603681
|
||||
--- /dev/null
|
||||
+++ b/posix/tst-wordexp-tilde.c
|
||||
@@ -0,0 +1,244 @@
|
||||
+/* Test wordexp tilde expansion with large usernames (BZ 34091).
|
||||
+ 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..f0f69ee85d 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,
|
||||
+ struct passwd pwd, *tpwd;
|
||||
+ int result;
|
||||
+ while ((result = __getpwnam_r (user,
|
||||
+ &pwd,
|
||||
+ tmpbuf.data + userlen + 1,
|
||||
+ tmpbuf.length - userlen - 1,
|
||||
&tpwd)) != 0
|
||||
&& errno == ERANGE)
|
||||
- if (!scratch_buffer_grow (&tmpbuf))
|
||||
- return WRDE_NOSPACE;
|
||||
+ {
|
||||
+ 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);
|
||||
Loading…
Reference in New Issue
Block a user