[PATCH v2] elf: Open the normalized $ORIGIN rpath in AT_SECURE programs (BZ 34360)

Adhemerval Zanella <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
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.
--
Changes from v1:
* Extracted the in-place normalizer from dl-load.c into a new header
  elf/dl-path-normalize.h.
* Added unit test elf/tst-dl-path-normalize.c.
* Fix the elf/tst-origin-secure.c secure handling.
---
 elf/Makefile                             |  18 ++
 elf/dl-load.c                            |  85 +++-------
 elf/dl-path-normalize.h                  | 114 +++++++++++++
 elf/libtst-origin-secure-mod.c           |  25 +++
 elf/tst-dl-path-normalize.c              | 142 ++++++++++++++++
 elf/tst-origin-secure-evilmod.c          |  28 ++++
 elf/tst-origin-secure-victim.c           |  43 +++++
 elf/tst-origin-secure.c                  | 203 +++++++++++++++++++++++
 elf/tst-origin-secure.h                  |  41 +++++
 elf/tst-origin-secure.root/postclean.req |   0
 10 files changed, 641 insertions(+), 58 deletions(-)
 create mode 100644 elf/dl-path-normalize.h
 create mode 100644 elf/libtst-origin-secure-mod.c
 create mode 100644 elf/tst-dl-path-normalize.c
 create mode 100644 elf/tst-origin-secure-evilmod.c
 create mode 100644 elf/tst-origin-secure-victim.c
 create mode 100644 elf/tst-origin-secure.c
 create mode 100644 elf/tst-origin-secure.h
 create mode 100644 elf/tst-origin-secure.root/postclean.req

diff --git a/elf/Makefile b/elf/Makefile
index dbc6cfec7fe..530776a3719 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -566,6 +566,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 \
@@ -591,6 +592,7 @@ tests-container += \
   tst-dlopen-self-container \
   tst-dlopen-tlsmodid-container \
   tst-ldconfig-cache \
+  tst-origin-secure \
   tst-pldd \
   tst-preload-pthread-libc \
   tst-ptrguard-static-dlopen \
@@ -599,6 +601,7 @@ tests-container += \
   # tests-container
 
 test-srcs = \
+  tst-origin-secure-victim \
   tst-pathopt \
   tst-sprof-basic \
   # tests-srcs
@@ -839,6 +842,7 @@ modules-names += \
   libtracemod3-1 \
   libtracemod4-1 \
   libtracemod5-1 \
+  libtst-origin-secure-mod \
   ltglobmod1 \
   ltglobmod2 \
   neededobj1 \
@@ -1034,6 +1038,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-ptrguard-static-dlopen-mod \
@@ -3766,3 +3771,16 @@ $(objpfx)tst-dl-debug-exclude.out: tst-dl-debug-exclude.sh \
 		 $(objpfx)tst-recursive-tls > $@; \
 	$(evaluate-test)
 endif
+
+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 95404adae94..471478649b7 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -34,6 +34,7 @@
 #include <gnu/lib-names.h>
 #include <dl-tunables.h>
 #include <dl-scratch-buffer.h>
+#include <dl-path-normalize.h>
 
 #include "dynamic-link.h"
 #include "get-dynamic-info.h"
@@ -91,67 +92,30 @@ 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;
-
-  struct dl_scratch_buffer scratch = dl_scratch_buffer_init ();
-  dl_scratch_buffer_allocate (&scratch, len + 2, 0);
-  char *npath = scratch.data;
-  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++ = '/';
-
-  bool result = false;
   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.  */
-	  result = true;
-	  break;
-	}
+      /* 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;
     }
 
-  dl_scratch_buffer_free (&scratch);
-  return result;
+  return false;
 }
 
 /* Given a substring starting at INPUT, just after the DST '$' start
@@ -335,16 +299,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 00000000000..96e04f5d14e
--- /dev/null
+++ b/elf/dl-path-normalize.h
@@ -0,0 +1,114 @@
+/* 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
+   <https://www.gnu.org/licenses/>.  */
+
+#ifndef _DL_PATH_NORMALIZE_H
+#define _DL_PATH_NORMALIZE_H
+
+#include <stddef.h>
+
+/* 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, the root '/' plus any preserved
+     leading ".." components of a relative path.  */
+  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: keep the unresolvable ".." for a
+		 relative path (it becomes part of the preserved prefix),
+		 drop it at the root of an absolute one.  */
+	      if (wnp > pstart)
+		*wnp++ = '/';
+	      *wnp++ = '.';
+	      *wnp++ = '.';
+	      limit = wnp;
+	    }
+	}
+      else
+	{
+	  if (wnp > pstart)
+	    *wnp++ = '/';
+	  while (rnp < rend)
+	    *wnp++ = *rnp++;
+	}
+
+      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 00000000000..8d7f3720944
--- /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
+   <https://www.gnu.org/licenses/>.  */
+
+#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 00000000000..f6d45d654fd
--- /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
+   <https://www.gnu.org/licenses/>.  */
+
+#include <dl-path-normalize.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/next_to_fault.h>
+
+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 <support/test-driver.c>
diff --git a/elf/tst-origin-secure-evilmod.c b/elf/tst-origin-secure-evilmod.c
new file mode 100644
index 00000000000..0913e32e21b
--- /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
+   <https://www.gnu.org/licenses/>.  */
+
+#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 00000000000..601265aaa40
--- /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
+   <https://www.gnu.org/licenses/>.  */
+
+#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 00000000000..f304823b498
--- /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
+   <https://www.gnu.org/licenses/>.  */
+
+
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/xunistd.h>
+#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 <support/test-driver.c>
diff --git a/elf/tst-origin-secure.h b/elf/tst-origin-secure.h
new file mode 100644
index 00000000000..2e2b9446072
--- /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
+   <https://www.gnu.org/licenses/>.  */
+
+#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 00000000000..e69de29bb2d
-- 
2.53.0
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.