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

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Fri Aug 14 13:00:19 GMT 2026


Ping.

On 15/07/26 16:52, Adhemerval Zanella wrote:
> 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 normalized
> path while opening the expansion.  For DT_PATH with "../" and if the an
> attacker who controls a component of $ORIGIN (e.g. by hard-linking the
> setuid binary into an attacker-owned directory), they can make the opened
> path escape the trusted directory even though the check passed.
> 
> Normalize the expansion in place and open that, so the path that is opened
> is exactly the path that was validated.  dst_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.
> 
> It also collapses runs of '/' before interpreting "/." and "/..", which
> is_trusted_path_normalize did not.  The old code normalized "/lib64//../x"
> to "/lib64/x" and checked that.  This was second way for the validated and
> the opened path to disagree.
> 
> The trusted directory check is split out into path_is_trusted, which
> matches the normalized path against the system_dirs entries on component
> boundaries, so it no longer needs a scratch buffer to append a trailing
> slash.
> 
> Checked on x86_64-linux-gnu and aarch64-linux-gnu.
> ---
>  elf/Makefile                             |  16 ++
>  elf/dl-load.c                            | 119 +++++++-----
>  elf/libtst-origin-secure-mod.c           |  25 +++
>  elf/tst-origin-secure-evilmod.c          |  28 +++
>  elf/tst-origin-secure-victim.c           |  48 +++++
>  elf/tst-origin-secure.c                  | 228 +++++++++++++++++++++++
>  elf/tst-origin-secure.h                  |  41 ++++
>  elf/tst-origin-secure.root/postclean.req |   0
>  8 files changed, 457 insertions(+), 48 deletions(-)
>  create mode 100644 elf/libtst-origin-secure-mod.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 94c5b7e6ed8..93f354d3c18 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -591,6 +591,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 +600,7 @@ tests-container += \
>    # tests-container
>  
>  test-srcs = \
> +  tst-origin-secure-victim \
>    tst-pathopt \
>    tst-sprof-basic \
>    # tests-srcs
> @@ -839,6 +841,7 @@ modules-names += \
>    libtracemod3-1 \
>    libtracemod4-1 \
>    libtracemod5-1 \
> +  libtst-origin-secure-mod \
>    ltglobmod1 \
>    ltglobmod2 \
>    neededobj1 \
> @@ -1034,6 +1037,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 \
> @@ -3734,3 +3738,15 @@ $(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 UP_LEVELS 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..f653804c5fb 100644
> --- a/elf/dl-load.c
> +++ b/elf/dl-load.c
> @@ -91,67 +91,82 @@ static const size_t system_dirs_len[] =
>  };
>  #define nsystem_dirs_len array_length (system_dirs_len)
>  
> -static bool
> -is_trusted_path_normalize (const char *path, size_t len)
> +/* Lexically normalize the NUL-terminated PATH in place, collapsing "//",
> +   "/./" and "/../" segments (a leading "/../" collapses to "/").  The
> +   normalized string is a rearrangement of a prefix of PATH: the write
> +   cursor never runs ahead of the read cursor and no trailing character is
> +   appended, so this only ever touches bytes within the original
> +   strlen (PATH) + 1 storage and can never access memory out of bounds.
> +   Returns the length of the normalized path (excluding the terminating
> +   NUL).  */
> +static size_t
> +dst_normalize_path (char *path)
>  {
> -  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')
> +  char *wnp = path;
> +  const char *rnp = path;
> +  while (*rnp != '\0')
>      {
> -      if (path[0] == '/')
> +      if (rnp[0] == '/')
>  	{
> -	  if (path[1] == '.')
> +	  /* Collapse a run of '/' to a single one before interpreting "/."
> +	     or "/..", so that the "." or ".." is applied to the real
> +	     preceding component rather than to an empty "//" segment: skip
> +	     this '/' whenever it is immediately followed by another one.  */
> +	  if (rnp[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;
> -		}
> +	      ++rnp;
> +	      continue;
>  	    }
>  
> -	  if (wnp > npath && wnp[-1] == '/')
> +	  if (rnp[1] == '.')
>  	    {
> -	      ++path;
> -	      continue;
> +	      if (rnp[2] == '.' && (rnp[3] == '/' || rnp[3] == '\0'))
> +		{
> +		  while (wnp > path && *--wnp != '/')
> +		    ;
> +		  rnp += 3;
> +		  continue;
> +		}
> +	      else if (rnp[2] == '/' || rnp[2] == '\0')
> +		{
> +		  rnp += 2;
> +		  continue;
> +		}
>  	    }
>  	}
>  
> -      *wnp++ = *path++;
> +      *wnp++ = *rnp++;
>      }
>  
> -  if (wnp == npath || wnp[-1] != '/')
> -    *wnp++ = '/';
> +  *wnp = '\0';
> +  return wnp - path;
> +}
>  
> -  bool result = false;
> +/* 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 '/' (e.g.
> +   "/lib64" and "/lib64/x" match "/lib64/" but "/lib64x" does not).  */
> +static bool
> +path_is_trusted (const char *npath, size_t nlen)
> +{
>    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'))
> +	/* Found it.  */
> +	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
> @@ -327,6 +342,8 @@ _dl_dst_substitute (struct link_map *l, const char *input, char *result)
>      }
>    while (*input != '\0');
>  
> +  *wp = '\0';
> +
>    /* In SUID/SGID programs, after $ORIGIN expansion the normalized
>       path must be rooted in one of the trusted directories.  The $LIB
>       and $PLATFORM DST cannot in any way be manipulated by the caller
> @@ -335,15 +352,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.
>  
> -  *wp = '\0';
> +     Checking the normalized path but opening the raw one is not enough:
> +     "a/b/../c" only names "a/c" when "b" is not a symbolic link, so an
> +     attacker who controls a component of $ORIGIN (for example by
> +     hard-linking the program into an attacker-owned directory) could
> +     otherwise redirect the lookup outside the trusted directory.  Replace
> +     the expansion with its normalized, "../"-free form, so that the path
> +     that is opened is exactly the path that was validated.  */
> +  if (__glibc_unlikely (check_for_trusted))
> +    {
> +      size_t nlen = dst_normalize_path (result);
> +      if (!path_is_trusted (result, nlen))
> +	*result = '\0';
> +    }
>  
>    return result;
>  }
> 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-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..50dafe3ec2e
> --- /dev/null
> +++ b/elf/tst-origin-secure-victim.c
> @@ -0,0 +1,48 @@
> +/* 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 <stdlib.h>
> +
> +#include "tst-origin-secure.h"
> +
> +/* Report both which module was loaded and whether the loader ran the program
> +   in secure mode, so the driver can tell a trusted-path bypass apart from a
> +   run that was not secure.
> +
> +   The glibc.rtld.enable_secure=1 tunable does not change getauxval
> +   (AT_SECURE), so check whether the loader strips GLIBC_TUNABLES instead.
> +
> +   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 (getenv ("GLIBC_TUNABLES") == NULL)
> +    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..f4b64e147e4
> --- /dev/null
> +++ b/elf/tst-origin-secure.c
> @@ -0,0 +1,228 @@
> +/* 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 from a private BASE directory two levels deep.  Lexically the
> +   five "../" pop BASE/sub back to "/", so 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.
> +
> +   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 <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 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"
> +
> +/* Number of "../" in the victim's rpath; it must match the -rpath option in
> +   the Makefile.  Two invariants:
> +
> +   - BASE must be at most UP_LEVELS - 1 levels deep, so that the "../" run
> +     pops $ORIGIN/sub all the way to "/" and the entry normalizes to
> +     SLIBDIR/SUBDIR.  BASE is created directly under the container's /tmp,
> +     so it is two levels deep.
> +
> +   - The "sub" symlink must point UP_LEVELS + 1 levels below BASE, so that
> +     the kernel lands in BASE/x1 rather than at "/".  */
> +#define UP_LEVELS 5
> +
> +/* $ORIGIN of the victim, and the victim itself; set up by do_prepare.  */
> +static char BASE[] = "/tmp/tst-origin-secureXXXXXX";
> +static char *victim_dst;
> +
> +/* With BASE the temporary directory created here and SLIBDIR "/lib64", the
> +   directory contains:
> +
> +     /lib64/tst-origin-secure/libtst-origin-secure-mod.so   trusted copy (id 1)
> +     BASE/victim                                            the executable
> +     BASE/sub -> BASE/x1/x2/x3/x4/x5/x6                     UP_LEVELS + 1 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 BASE/sub + five "../" + /lib64/tst-origin-secure.  */
> +static void
> +do_prepare (int argc, char **argv)
> +{
> +  const char *slibdir = support_slibdir_prefix;         /* e.g. "/lib64".  */
> +  const char *objelf = support_objdir_root;             /* build 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);
> +
> +  /* We will add a lot of directories and files, using
> +     support_create_temp_directory will required to register each on them
> +     in the correct order or adding a cleanup routine.  Rely on test-container
> +     cleanup instead.  */
> +  TEST_VERIFY_EXIT (mkdtemp (BASE) != NULL);
> +
> +  char *good_dir = xasprintf ("%s/%s", slibdir, SUBDIR);
> +  char *good_dst = xasprintf ("%s/%s", good_dir, SONAME);
> +  xmkdirp (good_dir, 0755);
> +
> +  char *evil_dir = xasprintf ("%s/x1%s/%s", BASE, slibdir, SUBDIR);
> +  char *evil_dst = xasprintf ("%s/%s", evil_dir, SONAME);
> +  xmkdirp (evil_dir, 0755);
> +
> +  victim_dst = xasprintf ("%s/victim", BASE);
> +  support_copy_file (good_src, good_dst);
> +  support_copy_file (evil_src, evil_dst);
> +  support_copy_file (victim_src, victim_dst);
> +  xchmod (victim_dst, 0755);
> +
> +  /* The target of the "sub" symlink, UP_LEVELS + 1 levels below BASE.  */
> +  char *sub_target = xstrdup (BASE);
> +  for (int i = 1; i <= UP_LEVELS + 1; ++i)
> +    {
> +      char *next = xasprintf ("%s/x%d", sub_target, i);
> +      free (sub_target);
> +      sub_target = next;
> +    }
> +  xmkdirp (sub_target, 0755);
> +
> +  char *sub = xasprintf ("%s/sub", BASE);
> +  xsymlink (sub_target, sub);
> +
> +  free (good_src);
> +  free (evil_src);
> +  free (victim_src);
> +  free (good_dir);
> +  free (good_dst);
> +  free (evil_dir);
> +  free (evil_dst);
> +  free (sub_target);
> +  free (sub);
> +}
> +#define PREPARE do_prepare
> +
> +
> +static int
> +run_victim (const char *victim, char *const *envp)
> +{
> +  char *const argv[] = { (char *) victim, NULL };
> +
> +  struct support_capture_subprocess proc
> +    = support_capture_subprogram (victim, argv, envp);
> +
> +  if (proc.err.length != 0)
> +    printf ("info: victim stderr: %s\n", proc.err.buffer);
> +
> +  int status = proc.status;
> +  support_capture_subprocess_free (&proc);
> +
> +  return WIFEXITED (status) ? WEXITSTATUS (status) : -1;
> +}
> +
> +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.  GLIBC_TUNABLES is passed here too.  */
> +  {
> +    char *const env[] = { (char *) "GLIBC_TUNABLES=glibc.rtld.enable_secure=0",
> +			  NULL };
> +    int rc = run_victim (victim_dst, 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 (victim_dst, 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 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 reports not being "
> +		    "secure: the GLIBC_TUNABLES proxy for secure mode in "
> +		    "tst-origin-secure-victim.c is no longer valid");
> +
> +      /* 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);
> +      }
> +  }
> +
> +  free (victim_dst);
> +  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..99d7f10df9d
> --- /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 /* tst-origin-secure.h */
> 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



More information about the Libc-alpha mailing list