Re: [PATCH] elf: Open the normalized $ORIGIN rpath in AT_SECURE programs (BZ 34360)
Adhemerval Zanella Netto <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Organization | Linaro |
| Message-ID | <[email protected]> |
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