From patchwork Tue Jun 16 12:49:25 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 137120 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 6749D4C91762 for ; Tue, 16 Jun 2026 12:50:26 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 6749D4C91762 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=F+2gA6zH X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dl1-x1233.google.com (mail-dl1-x1233.google.com [IPv6:2607:f8b0:4864:20::1233]) by sourceware.org (Postfix) with ESMTPS id D29FE4C91756 for ; Tue, 16 Jun 2026 12:49:52 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D29FE4C91756 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D29FE4C91756 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1233 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1781614193; cv=none; b=XwgUIRm7n423kqnoj+p5TnR+O6pvyLZRhpZryRA71vv/byjZYdOqKOx35NMAe1FEMJu0B04/v2MkuzfwEk4K+/WdbFwNRsy98y1CQqykB0FpAf0jOXluTP5oOHBAneVgkgyYgGbOJVg6r2vJjdt/XsQuiLf4LlR7EtBBDYYEroA= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1781614193; c=relaxed/simple; bh=VyJuIiMAux+rQE1RmADgV+9pohPiPwZ+LK+Bpfkl3yU=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=VOmOCLiy+lKwgikGObydygnULhoGNDOznVVQfshVJ5G2hkgY7hO0bL6XnpfAd9ZSoeuMfuXHyO7rMxU4mjyHJwfm4buYLccKsGwewGeYbUWv8fyJH/ji+45eb9d6lHJtsifYiTuNvjihp9VTEV2VJS3bHP+M/vx5OLhLi3k/Adw= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=F+2gA6zH DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D29FE4C91756 Received: by mail-dl1-x1233.google.com with SMTP id a92af1059eb24-13810b63a1aso10144049c88.1 for ; Tue, 16 Jun 2026 05:49:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1781614192; x=1782218992; darn=sourceware.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=Mlr7UrV5SDyowaPpHO+PNDUIjNXG/axuawdvDjwnqjc=; b=F+2gA6zHcmTBbE/N9kpZBmVVEs2qoFcELtCrM1/VExwh2v9DUHw6rmMyDFpKM0inEx R9WrOGvwXJpLBRKimx3rmAirdOUZFvp4nrHGLrdML8hsSjSJA2mJGrcqp26bkBq3L7gB T36LRTaU/rHniCLhDufFkv+a2YwWkwPo0Q62RipM//7wSzkkRaZReW7lZUDrhxn6f3pn SWkARS+60sC02Sjf1LnnwkCGGaIXGB++yIuRH/aBYbGcAxuXOyKqUXvi5LtVX20TEE4/ arCOjYLn8crlk6tW3CYl2vERyj1nlaYn6uuVrDCREID3wtNQ48E1WdbVdDMUegYMiPU+ 1UPA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1781614192; x=1782218992; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=Mlr7UrV5SDyowaPpHO+PNDUIjNXG/axuawdvDjwnqjc=; b=BVBtD8nW2wO0XJ9FfEgljMGwvAOqxXe4e/jvL+bNcD3QkdwKju9haWLrUcl4GKsTYj V7AabUHAp4QF+ewkNwEjOSC+ZP/omB+7xeduXOOu8StcmYAyiqU+y7W4EEtVy6DMvC4n U0tOM1qld29Awc/8totjg+f+iCZJScolAJ5W5xRMG01AGqr8x9um9ctIzfmuxUAIw8qc QNJtRLOZ9gXmULVPlWLX5Kokb74/Wd+EhFGOjOlX1hOLfMNYrAl1dvjtKuSpJTlTyuTR F74YD0ZoioGT5rg2D81vE6Y4O9iARZHyow78RtICEKoovm6/9e6nTor179qdsXxQfhmq NXMw== X-Gm-Message-State: AOJu0YxCicOAjJhShSheFrI6dlfoo1ncaYRxi4mOeHE/1z4xo3P9BBRT KqbAWeXkEVJtFNcVAFxFhvx1qh2nSGGw/W06xQz7nOvkVp6RMn1BsC4We6rOPeDzuzjw6p3irnH 859ZR X-Gm-Gg: Acq92OF1FIyXCA7+9Pcz3wMLt8ZLL/mwVbMnMQxeyf5mT5No8mwmlrk9SYRf3AldezU 82mHkMV6Yaemq9UG3qxBKoDAyqWWyiWNnGPSfx7aC+ODaAUxN9KCMfbGvZi5voseaaOYto1wi63 SvfAqyV/pt5pLgZqBQGUROwGbhrMGlKhZXwngRf4TgnePl+HdOVj1nv6cOSeQvAVUjbNBC3Un7f A5H0dQHe7qhCaQRnEmnN3t6fWybWJ8HxYKr11NnuKF9LvWimOBPovCdRF3PPl3Pvwgq/G0bY2nT FT0qpjW8OSORb8mKMjm4wjASHc1x+aU1G0QQ8gD48qbCHgQ6eppvj721ABnWxqcUIZxowXx/cRt tAVtF/Qjkrd0Wd+uuaN1hM5i2ISYApWfs02tA75HV/2NvgN9J85Zol1Iji394CdqivqRrHAQfsj mQv6169c7WggNxOzK87Ov2OLPZs1wtSkGNvGEiquslhDMw22Ijzyz9aI8jVwHR5kX6ltTRgAIpA g== X-Received: by 2002:a05:7022:f82:b0:137:8bc2:833 with SMTP id a92af1059eb24-13985f0856dmr1734265c88.19.1781614191448; Tue, 16 Jun 2026 05:49:51 -0700 (PDT) Received: from ubuntu-vm.. (200-153-194-206.dsl.telesp.net.br. [200.153.194.206]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-1384b8fbde7sm13293814c88.2.2026.06.16.05.49.50 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 16 Jun 2026 05:49:51 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: Carlos O'Donell Subject: [PATCH v2] posix: Fix stack overflow in wordexp tilde expansion (BZ 34091, CVE-2026-6791) Date: Tue, 16 Jun 2026 12:49:25 +0000 Message-ID: <20260616124946.1869657-1-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 MIME-Version: 1.0 X-Spam-Status: No, score=-10.8 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_BARRACUDACENTRAL, RCVD_IN_DNSWL_NONE, RCVD_IN_PBL, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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. Reviewed-by: Carlos O'Donell --- 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 | 24 +- 6 files changed, 268 insertions(+), 6 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 0fa532396fd..d9f897faa16 100644 --- a/posix/Makefile +++ b/posix/Makefile @@ -360,6 +360,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 00000000000..1661603681a --- /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 + . */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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 "~/tail" where 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 ~/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 diff --git a/posix/tst-wordexp-tilde.root/etc/group b/posix/tst-wordexp-tilde.root/etc/group new file mode 100644 index 00000000000..1dbf9013eeb --- /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 00000000000..098a8d59382 --- /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 00000000000..eb85a552ad2 --- /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 4a8541add49..f0f69ee85d5 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);