[glibc/release/2.41/master] posix: Fix stack overflow in wordexp tilde expansion (BZ 34091, CVE-2026-6791)

Aurelien Jarno via Glibc-cvs <[email protected]>
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=965b580070b8184b0dd390800d9194291a600e67

commit 965b580070b8184b0dd390800d9194291a600e67
Author: Adhemerval Zanella <[email protected]>
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.
    
    (cherry picked from commit 07c24f35392b727e6100d33edfdf811a6c68c218)

Diff:
---
 posix/Makefile                                 |   1 +
 posix/tst-wordexp-tilde.c                      | 244 +++++++++++++++++++++++++
 posix/tst-wordexp-tilde.root/etc/group         |   1 +
 posix/tst-wordexp-tilde.root/etc/nsswitch.conf |   3 +
 posix/tst-wordexp-tilde.root/etc/passwd        |   1 +
 posix/wordexp.c                                |  24 ++-
 6 files changed, 268 insertions(+), 6 deletions(-)

diff --git a/posix/Makefile b/posix/Makefile
index 9360bc7ff8..c2c05b3973 100644
--- a/posix/Makefile
+++ b/posix/Makefile
@@ -354,6 +354,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..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 9df4bb7424..731d1650e9 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);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.