From 622a9adf91ef81fcb84bc8e4fe5c46e4e0496b7d Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Tue, 6 Oct 2026 18:52:54 +0200 Subject: [PATCH] glibc: 2.42-100 -> 2.42-103; fixes CVE-2026-89092, CVE-2026-97399, CVE-2026-95818 Fixes #566198 for stable as well. --- .../libraries/glibc/2.42-master.patch | 1195 +++++++++++++++++ pkgs/development/libraries/glibc/common.nix | 4 +- 2 files changed, 1197 insertions(+), 2 deletions(-) diff --git a/pkgs/development/libraries/glibc/2.42-master.patch b/pkgs/development/libraries/glibc/2.42-master.patch index 7b9c5db6c0cf..50774f06f53b 100644 --- a/pkgs/development/libraries/glibc/2.42-master.patch +++ b/pkgs/development/libraries/glibc/2.42-master.patch @@ -12244,3 +12244,1198 @@ index 0ef5dde373..e25fbff351 100644 /* Clear all backing arrays. */ while (__environ_array_list != NULL) { + +commit 58927d6ca643e6c5b00625d7b5b323f7e14dc791 +Author: Adhemerval Zanella +Date: Mon Aug 17 15:15:32 2026 -0300 + + elf: Open the normalized $ORIGIN rpath in AT_SECURE programs (BZ 34360) + + For AT_SECURE programs the loader honors $ORIGIN in DT_RPATH only when the + expansion is rooted in a trusted directory, but it validated the lexically + normalized path while opening the raw expansion. As "a/b/../c" only names + "a/c" when "b" is not a symlink, an attacker who controls a component of + $ORIGIN -- e.g. by hard-linking the setuid binary into an attacker-owned + directory -- can make the opened path escape the trusted directory even + though the check passed, loading an attacker-controlled object. + + Normalize the expansion in place and open that, so the path that is opened + is exactly the path that was validated. _dl_normalize_path rewrites the + string in place without ever advancing its write cursor past its read + cursor or appending, so it stays within the original storage. + + Add elf/tst-origin-secure as a regression test. + + Reviewed-by: Florian Weimer + (cherry picked from commit ed0c137b97eb940b4b64981e84ed806d3276edd9) + +diff --git a/elf/Makefile b/elf/Makefile +index 3a5596e2bb..74a10671ef 100644 +--- a/elf/Makefile ++++ b/elf/Makefile +@@ -539,6 +539,7 @@ tests-internal += \ + tst-audit19a \ + tst-create_format1 \ + tst-dl-hwcaps_split \ ++ tst-dl-path-normalize \ + tst-dl_find_object \ + tst-dl_find_object-threads \ + tst-dlmopen2 \ +@@ -563,12 +564,14 @@ endif + tests-container += \ + tst-dlopen-self-container \ + tst-dlopen-tlsmodid-container \ ++ tst-origin-secure \ + tst-pldd \ + tst-preload-pthread-libc \ + tst-rootdir \ + # tests-container + + test-srcs = \ ++ tst-origin-secure-victim \ + tst-pathopt \ + tst-sprof-basic \ + # tests-srcs +@@ -791,6 +794,7 @@ modules-names += \ + libtracemod3-1 \ + libtracemod4-1 \ + libtracemod5-1 \ ++ libtst-origin-secure-mod \ + ltglobmod1 \ + ltglobmod2 \ + neededobj1 \ +@@ -986,6 +990,7 @@ modules-names += \ + tst-nodeps2-mod \ + tst-non-directory-mod \ + tst-null-argv-lib \ ++ tst-origin-secure-evilmod \ + tst-p_alignmod-base \ + tst-p_alignmod3 \ + tst-recursive-tlsmallocmod \ +@@ -3519,3 +3524,16 @@ $(objpfx)tst-origin.out: tst-origin.sh $(objpfx)tst-origin + $(evaluate-test) + + $(objpfx)tst-dlopen-sgid.out: $(objpfx)tst-dlopen-sgid-mod.so ++ ++LDFLAGS-libtst-origin-secure-mod.so += -Wl,-soname,libtst-origin-secure-mod.so ++LDFLAGS-tst-origin-secure-evilmod.so += -Wl,-soname,libtst-origin-secure-mod.so ++$(objpfx)tst-origin-secure-victim: $(objpfx)libtst-origin-secure-mod.so ++# The number of "../" here must match the layout invariants described in ++# tst-origin-secure.c. ++LDFLAGS-tst-origin-secure-victim += \ ++ -Wl,--no-as-needed \ ++ -Wl,-rpath,\$$ORIGIN/sub/../../../../..$(slibdir)/tst-origin-secure \ ++ -Wl,--disable-new-dtags ++$(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \ ++ $(objpfx)libtst-origin-secure-mod.so \ ++ $(objpfx)tst-origin-secure-evilmod.so +diff --git a/elf/dl-load.c b/elf/dl-load.c +index 00b9da9ec7..2be10ed7c7 100644 +--- a/elf/dl-load.c ++++ b/elf/dl-load.c +@@ -33,6 +33,7 @@ + #include + #include + #include ++#include + + /* Type for the buffer we put the ELF header and hopefully the program + header. This buffer does not really have to be too large. In most +@@ -114,54 +115,24 @@ static const size_t system_dirs_len[] = + }; + #define nsystem_dirs_len array_length (system_dirs_len) + ++/* Return true if the normalized path NPATH of length NLEN is rooted in one of ++ the trusted system directories. The system_dirs entries carry a trailing ++ '/'; NPATH matches an entry when it shares the entry's leading component ++ sequence and then either ends or continues with '/'. For instance, ++ "/lib64" and "/lib64/x" match "/lib64/" but "/lib64x" does not. */ + static bool +-is_trusted_path_normalize (const char *path, size_t len) ++path_is_trusted (const char *npath, size_t nlen) + { +- if (len == 0) +- return false; +- +- char *npath = (char *) alloca (len + 2); +- char *wnp = npath; +- while (*path != '\0') +- { +- if (path[0] == '/') +- { +- if (path[1] == '.') +- { +- if (path[2] == '.' && (path[3] == '/' || path[3] == '\0')) +- { +- while (wnp > npath && *--wnp != '/') +- ; +- path += 3; +- continue; +- } +- else if (path[2] == '/' || path[2] == '\0') +- { +- path += 2; +- continue; +- } +- } +- +- if (wnp > npath && wnp[-1] == '/') +- { +- ++path; +- continue; +- } +- } +- +- *wnp++ = *path++; +- } +- +- if (wnp == npath || wnp[-1] != '/') +- *wnp++ = '/'; +- + const char *trun = system_dirs; + + for (size_t idx = 0; idx < nsystem_dirs_len; ++idx) + { +- if (wnp - npath >= system_dirs_len[idx] +- && memcmp (trun, npath, system_dirs_len[idx]) == 0) +- /* Found it. */ ++ /* Compare against the entry without its trailing '/'. */ ++ size_t dirlen = system_dirs_len[idx] - 1; ++ ++ if (nlen >= dirlen ++ && memcmp (trun, npath, dirlen) == 0 ++ && (npath[dirlen] == '/' || npath[dirlen] == '\0')) + return true; + + trun += system_dirs_len[idx] + 1; +@@ -351,16 +322,21 @@ _dl_dst_substitute (struct link_map *l, const char *input, char *result) + checked for trust, the authors of the binaries themselves are + trusted to have designed this correctly. Only $ORIGIN is tested in + this way because it may be manipulated in some ways with hard +- links. */ +- if (__glibc_unlikely (check_for_trusted) +- && !is_trusted_path_normalize (result, wp - result)) +- { +- *result = '\0'; +- return result; +- } ++ links. ++ ++ _dl_normalize_path replaces the expansion with its normalized form ++ in place, so that the path that is opened is exactly the path that ++ was validated. */ + + *wp = '\0'; + ++ if (__glibc_unlikely (check_for_trusted)) ++ { ++ size_t nlen = _dl_normalize_path (result); ++ if (!path_is_trusted (result, nlen)) ++ *result = '\0'; ++ } ++ + return result; + } + +diff --git a/elf/dl-path-normalize.h b/elf/dl-path-normalize.h +new file mode 100644 +index 0000000000..ca4b14698c +--- /dev/null ++++ b/elf/dl-path-normalize.h +@@ -0,0 +1,118 @@ ++/* In-place lexical path normalization for the dynamic loader. ++ 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 ++ . */ ++ ++#ifndef _DL_PATH_NORMALIZE_H ++#define _DL_PATH_NORMALIZE_H ++ ++#include ++#include ++ ++/* Lexically normalize the null-terminated PATH in place and return the ++ length of the result (excluding the terminating NUL byte): ++ ++ - Runs of '/' are collapsed to a single '/'. ++ ++ - "." components are removed. ++ ++ - A ".." component removes the preceding component if there is one and it ++ is not itself a preserved "..". In an absolute path a surplus ".." at ++ the root is dropped ("/../a" normalizes to "/a"), in a relative path ++ leading ".." components are preserved ("../a" stays "../a", "a/../../b" ++ normalizes to "../b"). ++ ++ - The result has no trailing '/' except for the root path "/" itself ++ ("/a/" normalizes to "/a"). ++ ++ - The result is empty if and only if every component cancels or is removed ++ ("", ".", "a/.." all normalize to ""). ++ ++ The PATH is written in place, and the internal write cursor never runs ++ ahead of the read cursor. Only bytes within the strlen (PATH) + 1 storage ++ are accessed. */ ++static inline size_t ++_dl_normalize_path (char *path) ++{ ++ /* The root '/' of an absolute path is not removed. */ ++ char *pstart = path + (path[0] == '/'); ++ const char *rnp = pstart; ++ char *wnp = pstart; ++ /* End of the prefix a ".." may not remove. Either the root '/', or, for ++ relative paths, the original start of the string extended by any ++ preserved leading ".." components. */ ++ char *limit = pstart; ++ ++ while (*rnp != '\0') ++ { ++ /* Collapse consecutive separators. */ ++ if (*rnp == '/') ++ { ++ ++rnp; ++ continue; ++ } ++ ++ /* [RNP, REND) is the next input component. */ ++ const char *rend = rnp; ++ while (*rend != '\0' && *rend != '/') ++ ++rend; ++ size_t clen = rend - rnp; ++ ++ /* Drop '.' component. */ ++ if (clen == 1 && rnp[0] == '.') ++ ; ++ else if (clen == 2 && rnp[0] == '.' && rnp[1] == '.') ++ { ++ if (wnp > limit) ++ { ++ /* Remove the last component along with the '/' separating it ++ from its predecessor (the root '/' of an absolute path is ++ retained). */ ++ while (wnp > limit && wnp[-1] != '/') ++ --wnp; ++ if (wnp > pstart) ++ --wnp; ++ } ++ else if (pstart == path) ++ { ++ /* No component is left and the original path is relative: ++ keep the unresolvable ".." (it becomes part of the ++ preserved prefix). */ ++ if (wnp > pstart) ++ *wnp++ = '/'; ++ *wnp++ = '.'; ++ *wnp++ = '.'; ++ limit = wnp; ++ } ++ /* Otherwise the path is absolute and the surplus ".." at the ++ root is dropped ("/../a" normalizes to "/a"). */ ++ } ++ else ++ { ++ if (wnp > pstart) ++ *wnp++ = '/'; ++ memmove (wnp, rnp, clen); ++ wnp += clen; ++ } ++ ++ rnp = rend; ++ } ++ ++ *wnp = '\0'; ++ return wnp - path; ++} ++ ++#endif /* _DL_PATH_NORMALIZE_H */ +diff --git a/elf/libtst-origin-secure-mod.c b/elf/libtst-origin-secure-mod.c +new file mode 100644 +index 0000000000..8d7f372094 +--- /dev/null ++++ b/elf/libtst-origin-secure-mod.c +@@ -0,0 +1,25 @@ ++/* Module for tst-origin-secure (the "good" copy). ++ 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 "tst-origin-secure.h" ++ ++int ++origin_secure_id (void) ++{ ++ return ORIGIN_SECURE_ID_TRUSTED; ++} +diff --git a/elf/tst-dl-path-normalize.c b/elf/tst-dl-path-normalize.c +new file mode 100644 +index 0000000000..f6d45d654f +--- /dev/null ++++ b/elf/tst-dl-path-normalize.c +@@ -0,0 +1,142 @@ ++/* Unit tests for dl-path-normalize.h. ++ 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 ++ ++static void ++check_one_guarded (const char *input, const char *expected, bool before) ++{ ++ size_t size = strlen (input) + 1; ++ struct support_next_to_fault ntf ++ = before ? support_next_to_fault_allocate_before (size) ++ : support_next_to_fault_allocate (size); ++ memcpy (ntf.buffer, input, size); ++ ++ size_t len = _dl_normalize_path (ntf.buffer); ++ ++ TEST_COMPARE (len, strlen (ntf.buffer)); ++ TEST_COMPARE_STRING (ntf.buffer, expected); ++ ++ support_next_to_fault_free (&ntf); ++} ++ ++static void ++check_one (const char *input, const char *expected) ++{ ++ /* Check that _dl_normalize_path does not access the string outside the ++ input argument. It checks for both over-runs and under-runs (the ++ latter for the case of '..' expansions). */ ++ check_one_guarded (input, expected, false); ++ check_one_guarded (input, expected, true); ++} ++ ++static int ++do_test (void) ++{ ++ /* Absolute paths. */ ++ check_one ("/", "/"); ++ check_one ("//", "/"); ++ check_one ("///", "/"); ++ check_one ("////", "/"); ++ check_one ("/a", "/a"); ++ check_one ("/a/", "/a"); ++ check_one ("/a//", "/a"); ++ check_one ("//a//b//", "/a/b"); ++ check_one ("/.", "/"); ++ check_one ("/./", "/"); ++ check_one ("/./a", "/a"); ++ check_one ("/a/./b", "/a/b"); ++ check_one ("/a/.", "/a"); ++ check_one ("/..", "/"); ++ check_one ("/../", "/"); ++ check_one ("/../a", "/a"); ++ check_one ("/a/..", "/"); ++ check_one ("/a/../", "/"); ++ check_one ("/a/../..", "/"); ++ check_one ("/a/../b", "/b"); ++ check_one ("/a/../../b", "/b"); ++ check_one ("/a/b/../../c", "/c"); ++ check_one ("/a/b/../c", "/a/c"); ++ check_one ("/a/b/c/../..", "/a"); ++ check_one ("/usr/lib/../lib64", "/usr/lib64"); ++ check_one ("/usr/lib/../lib64/", "/usr/lib64"); ++ check_one ("/usr/lib/..//lib64/", "/usr/lib64"); ++ check_one ("/usr/lib/../../lib64/", "/lib64"); ++ ++ /* "." and ".." are special only as complete components. */ ++ check_one ("/a..", "/a.."); ++ check_one ("/..a", "/..a"); ++ check_one ("/a/...", "/a/..."); ++ check_one ("/.../a", "/.../a"); ++ check_one ("/a./b", "/a./b"); ++ check_one (".a", ".a"); ++ check_one ("a.", "a."); ++ check_one ("..a", "..a"); ++ check_one ("...", "..."); ++ ++ /* Relative paths. */ ++ check_one ("", ""); ++ check_one (".", ""); ++ check_one ("./", ""); ++ check_one ("..", ".."); ++ check_one ("../", ".."); ++ check_one ("a", "a"); ++ check_one ("a/", "a"); ++ check_one ("a//b", "a/b"); ++ check_one ("a..", "a.."); ++ check_one ("./a", "a"); ++ check_one ("./.", ""); ++ check_one ("./..", ".."); ++ ++ check_one ("a/..", ""); ++ check_one ("a/../", ""); ++ check_one ("ab/..", ""); ++ check_one (".a/..", ""); ++ check_one ("a./..", ""); ++ check_one (".../..", ""); ++ check_one ("a/./..", ""); ++ check_one ("a/b/..", "a"); ++ check_one ("abc/def/..", "abc"); ++ ++ /* Appending a component to an emptied relative output must not produce a ++ leading '/' (the path must stay relative). */ ++ check_one ("a/../b", "b"); ++ check_one ("a/.././b", "b"); ++ check_one ("a/../lib64/b", "lib64/b"); ++ ++ /* Leading ".." components of a relative path are preserved and stack ++ instead of cancelling each other; ordinary components may follow and be ++ removed again afterwards. */ ++ check_one ("../a", "../a"); ++ check_one ("../..", "../.."); ++ check_one ("../../..", "../../.."); ++ check_one ("../../a", "../../a"); ++ check_one ("../a/..", ".."); ++ check_one ("../../a/..", "../.."); ++ check_one ("a/../../b", "../b"); ++ check_one ("a/b/../../..", ".."); ++ ++ return 0; ++} ++ ++#include +diff --git a/elf/tst-origin-secure-evilmod.c b/elf/tst-origin-secure-evilmod.c +new file mode 100644 +index 0000000000..0913e32e21 +--- /dev/null ++++ b/elf/tst-origin-secure-evilmod.c +@@ -0,0 +1,28 @@ ++/* Module for tst-origin-secure (the attacker-controlled copy). ++ 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 "tst-origin-secure.h" ++ ++/* If the victim reports this copy, the loader opened the un-normalized rpath ++ and resolved it through the attacker's symlink -- i.e. the trusted-path ++ check was bypassed (bug 34360). */ ++int ++origin_secure_id (void) ++{ ++ return ORIGIN_SECURE_ID_ATTACKER; ++} +diff --git a/elf/tst-origin-secure-victim.c b/elf/tst-origin-secure-victim.c +new file mode 100644 +index 0000000000..601265aaa4 +--- /dev/null ++++ b/elf/tst-origin-secure-victim.c +@@ -0,0 +1,43 @@ ++/* Victim program for tst-origin-secure. ++ 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 "tst-origin-secure.h" ++ ++extern int __libc_enable_secure; ++ ++/* Report both which module was loaded and whether the loader ran in secure ++ mode, so the driver can tell a genuine trusted-path bypass apart from a run ++ that simply was not secure. ++ ++ The exit status is the combination of the ORIGIN_SECURE_STATUS_* bits: ++ ++ _NONE trusted copy, not secure ++ _ATTACKER attacker copy, not secure (the control run) ++ _SECURE trusted copy, secure (a fixed loader) ++ _SECURE | _ATTACKER attacker copy, secure (the bug: the raw ++ rpath was opened) */ ++int ++main (void) ++{ ++ int status = ORIGIN_SECURE_STATUS_NONE; ++ if (origin_secure_id () == ORIGIN_SECURE_ID_ATTACKER) ++ status |= ORIGIN_SECURE_STATUS_ATTACKER; ++ if (__libc_enable_secure != 0) ++ status |= ORIGIN_SECURE_STATUS_SECURE; ++ return status; ++} +diff --git a/elf/tst-origin-secure.c b/elf/tst-origin-secure.c +new file mode 100644 +index 0000000000..f304823b49 +--- /dev/null ++++ b/elf/tst-origin-secure.c +@@ -0,0 +1,203 @@ ++/* Test that AT_SECURE $ORIGIN rpath entries are looked up using the ++ normalized (trusted) path, not the raw expansion (bug 34360). ++ ++ 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 ++ . */ ++ ++ ++/* For a SUID/SGID program the loader only honors $ORIGIN in DT_RPATH when ++ the normalized expansion is rooted in a trusted directory. If the loader ++ opens the un-normalized string (that contains "../"), it might disagree ++ as soon as a path component is a symbolic link. ++ ++ This test builds the executable with the rpath: ++ ++ $ORIGIN/sub/../../../../..SLIBDIR/tst-origin-secure ++ ++ and runs it as BASE/a/b/victim, so $ORIGIN is BASE/a/b. Lexically the ++ five "../" pop $ORIGIN/sub back to "/" (BASE is /tmp/tst-origin-secure, ++ so $ORIGIN/sub is the five components tmp, tst-origin-secure, a, b, sub), ++ and the entry normalizes to the trusted SLIBDIR/tst-origin-secure. But ++ "sub" is a symlink pointing six levels deep under BASE, so opening the raw ++ string makes the kernel resolve the "../" through the symlink and land in ++ BASE/x1 SLIBDIR/tst-origin-secure instead. ++ ++ The "../" count in the rpath (see the Makefile) is therefore ++ depth(BASE) + 2 (for the "a/b" of $ORIGIN) + 1 (for "sub"); it is ++ independent of SLIBDIR, which is appended whole on both the raw and the ++ normalized side. ++ ++ A trusted copy of the module (ORIGIN_SECURE_ID_TRUSTED) is installed in ++ SLIBDIR/tst-origin-secure; an attacker copy (ORIGIN_SECURE_ID_ATTACKER) is ++ placed at the symlink-diverted location. The trusted subdirectory is ++ rooted under SLIBDIR (so it passes the trusted-path check) but is not ++ itself a default loader search directory. ++ ++ The victim reports, in its exit status, both which module it loaded and ++ whether it ran in secure mode. ++ ++ Secure mode is forced with glibc.rtld.enable_secure=1 so that no real ++ SUID/SGID binary is required. */ ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include "tst-origin-secure.h" ++ ++#define BASE "/tmp/tst-origin-secure" ++#define SONAME "libtst-origin-secure-mod.so" ++/* Subdirectory of the trusted SLIBDIR that the rpath normalizes to. It is ++ trusted (rooted under SLIBDIR) but not a default search directory. */ ++#define SUBDIR "tst-origin-secure" ++ ++static int ++run_victim (char *const *envp) ++{ ++ const char *victim = BASE "/a/b/victim"; ++ char *const argv[] = { (char *) victim, NULL }; ++ ++ struct support_capture_subprocess res ++ = support_capture_subprogram (victim, argv, envp); ++ /* The victim itself prints nothing; forward any loader diagnostics to ++ the test log. */ ++ if (res.err.length > 0) ++ printf ("info: victim stderr: %s\n", res.err.buffer); ++ int status = res.status; ++ support_capture_subprocess_free (&res); ++ return WIFEXITED (status) ? WEXITSTATUS (status) : -1; ++} ++ ++/* With SLIBDIR "/lib64" the container layout is: ++ ++ /lib64/tst-origin-secure/libtst-origin-secure-mod.so trusted copy (id 1) ++ BASE/a/b/victim the executable ++ BASE/a/b/sub -> BASE/x1/x2/x3/x4/x5/x6 six levels deep ++ BASE/x1/lib64/tst-origin-secure/libtst-origin-secure-mod.so ++ attacker copy (id 2) ++ BASE/x1/x2/x3/x4/x5/x6/ the symlink target ++ ++ The victim's rpath is $ORIGIN/sub + five "../" + /lib64/tst-origin-secure. */ ++static void ++do_prepare (int argc, char **argv) ++{ ++ const char *slibdir = support_slibdir_prefix; ++ const char *objelf = support_objdir_root; ++ ++ char *good_src = xasprintf ("%s/elf/libtst-origin-secure-mod.so", objelf); ++ char *evil_src = xasprintf ("%s/elf/tst-origin-secure-evilmod.so", objelf); ++ char *victim_src = xasprintf ("%s/elf/tst-origin-secure-victim", objelf); ++ ++ xmkdirp (BASE "/a/b", 0755); ++ xmkdirp (BASE "/x1/x2/x3/x4/x5/x6", 0755); ++ ++ /* Where the trusted copy lives (reached only via the normalized rpath, ++ SLIBDIR/SUBDIR) ... */ ++ char *good_dir = xasprintf ("%s/%s", slibdir, SUBDIR); ++ char *good_dst = xasprintf ("%s/%s", good_dir, SONAME); ++ xmkdirp (good_dir, 0755); ++ /* ... and where the raw, symlink-diverted lookup lands. */ ++ char *evil_dir = xasprintf ("%s/x1%s/%s", BASE, slibdir, SUBDIR); ++ char *evil_dst = xasprintf ("%s/%s", evil_dir, SONAME); ++ xmkdirp (evil_dir, 0755); ++ ++ /* support_copy_file preserves the source mode, so the victim stays ++ executable and the modules readable; no chmod is needed. */ ++ support_copy_file (good_src, good_dst); ++ support_copy_file (evil_src, evil_dst); ++ support_copy_file (victim_src, BASE "/a/b/victim"); ++ ++ unlink (BASE "/a/b/sub"); ++ xsymlink (BASE "/x1/x2/x3/x4/x5/x6", BASE "/a/b/sub"); ++ ++ free (good_src); ++ free (evil_src); ++ free (victim_src); ++ free (good_dir); ++ free (good_dst); ++ free (evil_dir); ++ free (evil_dst); ++} ++#define PREPARE do_prepare ++ ++static int ++do_test (void) ++{ ++ /* Control run: in normal mode $ORIGIN is honored without the trusted check, ++ so the raw rpath resolves through "sub" and the attacker copy is ++ loaded. */ ++ { ++ char *const env[] = { NULL }; ++ int rc = run_victim (env); ++ if (rc != ORIGIN_SECURE_STATUS_ATTACKER) ++ FAIL_EXIT1 ("control run returned status %d, expected %d (attacker " ++ "copy, not secure): the $ORIGIN layout does not reproduce " ++ "the divergence between the raw and the normalized rpath", ++ rc, ORIGIN_SECURE_STATUS_ATTACKER); ++ } ++ ++ /* Secure run: force AT_SECURE. A fixed loader normalizes the rpath to the ++ trusted SLIBDIR/SUBDIR and loads the trusted copy; a loader with the bug ++ opens the raw path, resolves "sub", and loads the attacker copy. */ ++ { ++ char *const env[] = { (char *) "GLIBC_TUNABLES=glibc.rtld.enable_secure=1", ++ NULL }; ++ int rc = run_victim (env); ++ switch (rc) ++ { ++ /* Secure, trusted copy loaded via the normalized rpath: fixed. */ ++ case ORIGIN_SECURE_STATUS_SECURE: ++ break; ++ ++ /* Secure, attacker copy loaded: the raw rpath was opened. */ ++ case ORIGIN_SECURE_STATUS_SECURE | ORIGIN_SECURE_STATUS_ATTACKER: ++ FAIL_EXIT1 ("secure-mode loader resolved the un-normalized rpath " ++ "through the attacker symlink (bug 34360)"); ++ ++ /* Not secure, attacker copy: exactly what the control run produced, so ++ the tunable did not engage and this run says nothing about the ++ trusted-path handling. */ ++ case ORIGIN_SECURE_STATUS_ATTACKER: ++ FAIL_UNSUPPORTED ("glibc.rtld.enable_secure=1 did not enable " ++ "secure mode (victim status %d)", rc); ++ ++ /* Not secure, yet the trusted copy was loaded, which is reachable only ++ through the normalized rpath, and only a secure loader normalizes it. ++ Fail rather than report UNSUPPORTED. */ ++ case ORIGIN_SECURE_STATUS_NONE: ++ FAIL_EXIT1 ("secure run loaded the trusted copy but the victim " ++ "reports not being secure: __libc_enable_secure is no " ++ "longer a valid proxy for secure mode"); ++ ++ /* Neither copy loaded: since the trusted copy is reachable only through ++ the normalized rpath, this means the rpath entry was not honored at ++ all. */ ++ default: ++ FAIL_EXIT1 ("secure run did not load the module via the normalized " ++ "rpath (victim status %d)", rc); ++ } ++ } ++ ++ return 0; ++} ++ ++#include +diff --git a/elf/tst-origin-secure.h b/elf/tst-origin-secure.h +new file mode 100644 +index 0000000000..2e2b944607 +--- /dev/null ++++ b/elf/tst-origin-secure.h +@@ -0,0 +1,41 @@ ++/* Definitions shared by the tst-origin-secure test, its victim and modules. ++ 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 ++ . */ ++ ++#ifndef _TST_ORIGIN_SECURE_H ++#define _TST_ORIGIN_SECURE_H 1 ++ ++enum ++ { ++ ORIGIN_SECURE_ID_TRUSTED = 1, /* The copy installed in the trusted ++ SLIBDIR. */ ++ ORIGIN_SECURE_ID_ATTACKER = 2, /* The copy reachable only by resolving the ++ "sub" symlink. */ ++ }; ++ ++extern int origin_secure_id (void); ++ ++enum ++ { ++ ORIGIN_SECURE_STATUS_NONE = 0, ++ ORIGIN_SECURE_STATUS_ATTACKER = 1 << 0, /* The victim loaded attacker ++ rather than the trusted. */ ++ ORIGIN_SECURE_STATUS_SECURE = 1 << 1, /* The loader ran the victim in ++ secure mode. */ ++ }; ++ ++#endif +diff --git a/elf/tst-origin-secure.root/postclean.req b/elf/tst-origin-secure.root/postclean.req +new file mode 100644 +index 0000000000..e69de29bb2 + +commit 9ff7ecb4cd1c12c6b8b24653ecf44841fb866be5 +Author: DJ Delorie +Date: Mon Sep 28 14:27:16 2026 +0200 + + CVE-2026-89092: nscd: replace alloca with malloc in aicache, hstcache + + This changes the alloca fallback to a malloc fallback + in the addrinfo and host cache code. + + This fixes bug 34624. + + Co-authored-by: Florian Weimer + Reviewed-by: Adhemerval Zanella + (cherry picked from commit cbf59b836645b6ac69ad8c47a560598bc4942ecc) + +diff --git a/nscd/aicache.c b/nscd/aicache.c +index 6f47236627..1ac86b86d8 100644 +--- a/nscd/aicache.c ++++ b/nscd/aicache.c +@@ -93,7 +93,9 @@ addhstaiX (struct database_dyn *db, int fd, request_header *req, + int32_t ttl = INT32_MAX; + ssize_t total = 0; + char *key_copy = NULL; +- bool alloca_used = false; ++ /* If not NULL, dataset is a temporary dataset not inserted into the ++ database, and needs to be free'd. */ ++ void *dataset_alloc = NULL; + time_t timeout = MAX_TIMEOUT_VALUE; + + while (!no_more) +@@ -164,10 +166,12 @@ addhstaiX (struct database_dyn *db, int fd, request_header *req, + /* We cannot permanently add the result in the moment. But + we can provide the result as is. Store the data in some + temporary memory. */ +- dataset = (struct dataset *) alloca (total + req->key_len); ++ dataset = (struct dataset *) malloc (total + req->key_len); ++ if (dataset == NULL) ++ goto out; + + /* We cannot add this record to the permanent database. */ +- alloca_used = true; ++ dataset_alloc = dataset; + } + + /* Fill in the address and address families. */ +@@ -336,10 +340,12 @@ addhstaiX (struct database_dyn *db, int fd, request_header *req, + /* We cannot permanently add the result in the moment. But + we can provide the result as is. Store the data in some + temporary memory. */ +- dataset = (struct dataset *) alloca (total + req->key_len); ++ dataset = (struct dataset *) malloc (total + req->key_len); ++ if (dataset == NULL) ++ goto out; + + /* We cannot add this record to the permanent database. */ +- alloca_used = true; ++ dataset_alloc = dataset; + } + + /* Fill in the address and address families. */ +@@ -391,8 +397,7 @@ addhstaiX (struct database_dyn *db, int fd, request_header *req, + resp)) == 0) + { + /* The data has not changed. We will just bump the +- timeout value. Note that the new record has been +- allocated on the stack and need not be freed. */ ++ timeout value. */ + dh->timeout = dataset->head.timeout; + dh->ttl = dataset->head.ttl; + ++dh->nreloads; +@@ -410,7 +415,9 @@ addhstaiX (struct database_dyn *db, int fd, request_header *req, + key_copy = (char *) newp + (key_copy - (char *) dataset); + + dataset = memcpy (newp, dataset, total + req->key_len); +- alloca_used = false; ++ ++ free (dataset_alloc); ++ dataset_alloc = NULL; + } + + /* Mark the old record as obsolete. */ +@@ -488,7 +495,7 @@ next_nip: + out: + __resolv_context_put (ctx); + +- if (dataset != NULL && !alloca_used) ++ if (dataset != NULL && dataset_alloc == NULL) + { + /* If necessary, we also propagate the data to disk. */ + if (db->persistent) +@@ -514,6 +521,8 @@ next_nip: + scratch_buffer_free (&tmpbuf4); + scratch_buffer_free (&canonbuf); + ++ free (dataset_alloc); ++ + return timeout; + } + +diff --git a/nscd/hstcache.c b/nscd/hstcache.c +index 7b3a8ab1ea..11af5cb34a 100644 +--- a/nscd/hstcache.c ++++ b/nscd/hstcache.c +@@ -15,7 +15,6 @@ + You should have received a copy of the GNU General Public License + along with this program; if not, see . */ + +-#include + #include + #include + #include +@@ -187,7 +186,7 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + /* Determine the I/O structure. */ + size_t h_name_len = strlen (hst->h_name) + 1; + size_t h_aliases_cnt; +- uint32_t *h_aliases_len; ++ uint32_t *h_aliases_len = NULL; + size_t h_addr_list_cnt; + char *addresses; + char *aliases; +@@ -201,7 +200,13 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + for (cnt = 0; hst->h_aliases[cnt] != NULL; ++cnt) + ++h_aliases_cnt; + /* Determine the length of all aliases. */ +- h_aliases_len = (uint32_t *) alloca (h_aliases_cnt * sizeof (uint32_t)); ++ if (h_aliases_cnt > 0) ++ { ++ h_aliases_len = (uint32_t *) reallocarray (NULL, h_aliases_cnt, ++ sizeof (uint32_t)); ++ if (h_aliases_len == NULL) ++ return MAX_TIMEOUT_VALUE; ++ } + total = 0; + for (cnt = 0; cnt < h_aliases_cnt; ++cnt) + { +@@ -215,8 +220,11 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + ++h_addr_list_cnt; + + if (h_addr_list_cnt == 0) +- /* Invalid entry. */ +- return MAX_TIMEOUT_VALUE; ++ { ++ /* Invalid entry. */ ++ free (h_aliases_len); ++ return MAX_TIMEOUT_VALUE; ++ } + + total += (sizeof (struct dataset) + + h_name_len +@@ -227,8 +235,10 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + change. Allocate memory on the cache since it is likely + discarded anyway. If it turns out to be necessary to have a + new record we can still allocate real memory. */ +- bool alloca_used = false; + dataset = NULL; ++ /* If not NULL, dataset is a temporary dataset not inserted into ++ the database, and needs to be free'd. */ ++ void *dataset_alloc = NULL; + + /* If the record contains more than one IP address (used for + load balancing etc) don't cache the entry. This is something +@@ -244,10 +254,15 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + /* We cannot permanently add the result in the moment. But + we can provide the result as is. Store the data in some + temporary memory. */ +- dataset = (struct dataset *) alloca (total + req->key_len); ++ dataset = (struct dataset *) malloc (total + req->key_len); ++ if (dataset == NULL) ++ { ++ free (h_aliases_len); ++ return MAX_TIMEOUT_VALUE; ++ } + + /* We cannot add this record to the permanent database. */ +- alloca_used = true; ++ dataset_alloc = dataset; + } + + timeout = datahead_init_pos (&dataset->head, total + req->key_len, +@@ -270,7 +285,8 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + cp = dataset->strdata; + + cp = mempcpy (cp, hst->h_name, h_name_len); +- cp = mempcpy (cp, h_aliases_len, h_aliases_cnt * sizeof (uint32_t)); ++ if (h_aliases_cnt > 0) ++ cp = mempcpy (cp, h_aliases_len, h_aliases_cnt * sizeof (uint32_t)); + + /* The normal addresses first. */ + addresses = cp; +@@ -282,6 +298,8 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + for (cnt = 0; cnt < h_aliases_cnt; ++cnt) + cp = mempcpy (cp, hst->h_aliases[cnt], h_aliases_len[cnt]); + ++ free (h_aliases_len); ++ + assert (cp + == dataset->strdata + total - offsetof (struct dataset, + strdata)); +@@ -307,8 +325,7 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + dh->allocsize - offsetof (struct dataset, resp)) == 0) + { + /* The data has not changed. We will just bump the +- timeout value. Note that the new record has been +- allocated on the stack and need not be freed. */ ++ timeout value. */ + assert (h_addr_list_cnt == 1); + dh->ttl = dataset->head.ttl; + dh->timeout = dataset->head.timeout; +@@ -334,7 +351,9 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + key_copy = (char *) newp + (key_copy - (char *) dataset); + + dataset = memcpy (newp, dataset, total + req->key_len); +- alloca_used = false; ++ ++ free (dataset_alloc); ++ dataset_alloc = NULL; + } + } + +@@ -362,7 +381,7 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + the current cache handling cannot handle and it is more than + questionable whether it is worthwhile complicating the cache + handling just for handling such a special case. */ +- if (! alloca_used) ++ if (dataset_alloc == NULL) + { + /* If necessary, we also propagate the data to disk. */ + if (db->persistent) +@@ -393,6 +412,8 @@ cache_addhst (struct database_dyn *db, int fd, request_header *req, + + pthread_rwlock_unlock (&db->lock); + } ++ free (dataset_alloc); ++ dataset_alloc = NULL; + } + + if (__builtin_expect (!all_written, 0) && debug_level > 0) + +commit e39d4ded3f28903fd2a504da8dfd136bcd285477 +Author: Sachin Monga +Date: Thu Oct 1 00:51:23 2026 +0530 + + powerpc: Fix one byte overread in strncasecmp (bug 34683, CVE-2026-97399) + + The Power8 strncasecmp (strcasecmp.S with USE_AS_STRNCASECMP) touches + s1[n]/s2[n] in three places when the first n bytes compare equal and + contain no NUL: L(loop) preloads the next byte before its bdnz; + L(loop1) increments then loads before its bdnz; and L(bytebybyte) loads + s1[0]/s2[0] before checking r5. A caller that maps exactly n readable bytes + can get SIGSEGV if byte n falls on an unmapped page. + + The L(bytebybyte) site was identified by Florian Weimer; the + original AISLE report covered only the two loops. + + Fix all three sites: guard L(bytebybyte) with a r5 == 0 check, split the + bdnz/load pairs in L(loop) and L(loop1) so the load only runs when the + count is still positive, and preload rCHAR1/rCHAR2 in L(lessthan4) after + confirming r5 != 0. strcasecmp is unaffected; its generated code is + unchanged. + + Add string/test-strncasecmp-nonarray.c, built on the test-Xncmp-nonarray.c + skeleton: maps a PROT_NONE page past both buffers and sweeps all start + offsets and lengths. Runs as an xtest. + + Reported-by: AISLE in partnership with Red Hat + Suggested-by: Florian Weimer + (cherry picked from commit e622f82ea8797704ac8f761f88ed3e429e97fedd) + +diff --git a/string/Makefile b/string/Makefile +index c83b195e2e..d7437cd915 100644 +--- a/string/Makefile ++++ b/string/Makefile +@@ -238,8 +238,8 @@ endif + # This test allocates a lot of memory and can run for a long time. + xtests += tst-strcoll-overflow + +-# This test runs for a long time. +-xtests += test-strncmp-nonarray ++# These tests run for a long time. ++xtests += test-strncasecmp-nonarray test-strncmp-nonarray + + # This test needs libdl. + ifeq (yes,$(build-shared)) +diff --git a/string/test-strncasecmp-nonarray.c b/string/test-strncasecmp-nonarray.c +new file mode 100644 +index 0000000000..d68e58d5ec +--- /dev/null ++++ b/string/test-strncasecmp-nonarray.c +@@ -0,0 +1,4 @@ ++#define TEST_IDENTIFIER strncasecmp ++#define TEST_NAME "strncasecmp" ++typedef char CHAR; ++#include "test-Xncmp-nonarray.c" +diff --git a/sysdeps/powerpc/powerpc64/power8/strcasecmp.S b/sysdeps/powerpc/powerpc64/power8/strcasecmp.S +index 7ce53407b8..838b8bf75f 100644 +--- a/sysdeps/powerpc/powerpc64/power8/strcasecmp.S ++++ b/sysdeps/powerpc/powerpc64/power8/strcasecmp.S +@@ -355,6 +355,14 @@ L(bytebybyte): + offset and string descriptors are only updated in the end + of loop unrolling. */ + ld rLOC, LOCALE_CTYPE_TOLOWER(rLOC) ++#ifdef USE_AS_STRNCASECMP ++ /* This entry point can be reached from the vector loop with ++ r5 == 0 (the last vector chunk ended exactly at the `n' ++ boundary). Do not read s1[0]/s2[0] in that case: those ++ bytes are s1[n]/s2[n], one past the caller's bound. */ ++ cmpdi cr7, r5, 0 ++ beq cr7, L(retnull) ++#endif + lbz rCHAR1, 0(rSTR1) /* Load char from s1 */ + lbz rCHAR2, 0(rSTR2) /* Load char from s2 */ + #ifdef USE_AS_STRNCASECMP +@@ -405,19 +413,23 @@ L(loop): + cmpw cr1, rLWR1, rLWR2 + crorc 4*cr1+eq,eq,4*cr1+eq + beq cr1, L(done) ++#ifdef USE_AS_STRNCASECMP ++ /* Test the remaining count before touching the next byte. */ ++ bdnz L(loop_load) ++ b L(lessthan4) ++L(loop_load): ++#endif + lbz rCHAR1, 0(rSTR1) /* Load char from s1 */ + lbz rCHAR2, 0(rSTR2) /* Load char from s2 */ +-#ifdef USE_AS_STRNCASECMP +- bdnz L(loop) +-#else + b L(loop) +-#endif + #ifdef USE_AS_STRNCASECMP + L(lessthan4): + clrldi r5, r5, 62 + cmpdi cr7, r5, 0 + beq cr7, L(retnull) + mtctr r5 ++ lbz rCHAR1, 0(rSTR1) ++ lbz rCHAR2, 0(rSTR2) + L(loop1): + cmpdi rCHAR1, 0 + sldi rADDR1, rCHAR1, 2 +@@ -427,11 +439,14 @@ L(loop1): + cmpw cr1, rLWR1, rLWR2 + crorc 4*cr1+eq,eq,4*cr1+eq + beq cr1, L(done) ++ bdnz L(loop1_load) ++ b L(retnull) ++L(loop1_load): + addi rSTR1, rSTR1, 1 + addi rSTR2, rSTR2, 1 + lbz rCHAR1, 0(rSTR1) + lbz rCHAR2, 0(rSTR2) +- bdnz L(loop1) ++ b L(loop1) + #endif + L(done): + subf r0, rLWR2, rLWR1 diff --git a/pkgs/development/libraries/glibc/common.nix b/pkgs/development/libraries/glibc/common.nix index 8b2d7f83f950..466cb2af4c55 100644 --- a/pkgs/development/libraries/glibc/common.nix +++ b/pkgs/development/libraries/glibc/common.nix @@ -51,7 +51,7 @@ let version = "2.42"; - patchSuffix = "-100"; + patchSuffix = "-103"; sha256 = "sha256-0XdeMuRijmTvkw9DW2e7Y691may2viszW58Z8WUJ8X8="; in @@ -69,7 +69,7 @@ stdenv.mkDerivation ( /* No tarballs for stable upstream branch, only https://sourceware.org/git/glibc.git and using git would complicate bootstrapping. $ git fetch --all -p && git checkout origin/release/2.42/master && git describe - glibc-2.42-100-gc7169c0684 + glibc-2.42-103-ge39d4ded3f $ git show --minimal --reverse glibc-2.42.. ':!ADVISORIES' > 2.42-master.patch To compare the archive contents zdiff can be used.