Re: [PATCH 2/3] elf: test AT_EXECFD consumption through a binfmt_misc 'O' handler
Adhemerval Zanella Netto <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Organization | Linaro |
| Message-ID | <[email protected]> |
On 15/07/26 07:13, Christian Brauner wrote: > Register the dynamic linker under test as a binfmt_misc extension > handler with the 'O' (open-binary) flag inside a private user and > mount namespace - binfmt_misc instances are per-user-namespace since > Linux 6.7, so nothing leaks to the host and the test is parallel-safe. > Kernels without sandboxed binfmt_misc mounts report UNSUPPORTED. > > The helper executed through the handler verifies the contract: > > - the application-visible argument vector is exactly what a direct > execution produces (the splice is consumed by ld.so as usual), > - getauxval (AT_EXECFD) reports the entry as absent: the descriptor > was consumed, closed, and neutralized to AT_IGNORE, > - AT_EXECFN is the original path, > - LD_TRACE_LOADED_OBJECTS lists the dependencies of the > descriptor-loaded main program, > - an execute-only (--x) copy still runs after the test sheds > CAP_DAC_OVERRIDE/CAP_DAC_READ_SEARCH, while a path open fails > with EACCES - proving the program really is loaded from the > descriptor and not re-opened by path. > > The namespace setup maps uid/gid 0 (unshare -r style) rather than > using support_become_root, which identity-maps the original uid: the > binfmt_misc inodes are owned by the namespace's uid 0, and an > unmapped owner cannot pass the permission checks for the register > file. > > Signed-off-by: Christian Brauner (Amutable) <[email protected]> > --- > sysdeps/unix/sysv/linux/Makefile | 5 + > sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c | 43 ++++ > sysdeps/unix/sysv/linux/tst-rtld-execfd.c | 282 +++++++++++++++++++++++++ > 3 files changed, 330 insertions(+) > > diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile > index 56b160e253..d1abf2b563 100644 > --- a/sysdeps/unix/sysv/linux/Makefile > +++ b/sysdeps/unix/sysv/linux/Makefile > @@ -697,6 +697,7 @@ $(objpfx)pldd: $(objpfx)xmalloc.o > tests += \ > tst-rseq-tls-range \ > tst-rseq-tls-range-4096 \ > + tst-rtld-execfd \ > tst-thp-1 \ > tst-thp-1-pde \ > tst-thp-1-static \ > @@ -707,6 +708,10 @@ tests-static += \ > tst-rseq-tls-range-static \ > tst-thp-1-static \ > # tests-static > +test-srcs += \ > + tst-rtld-execfd-prog \ > +# test-srcs > +$(objpfx)tst-rtld-execfd.out: $(objpfx)tst-rtld-execfd-prog > modules-names += \ > tst-rseq-tls-range-mod \ > tst-thp-size-mod \ > diff --git a/sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c b/sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c > new file mode 100644 > index 0000000000..e209279255 > --- /dev/null > +++ b/sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c > @@ -0,0 +1,43 @@ > +/* Helper program for tst-rtld-execfd: report argv and AT_EXECFD state. > + 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; see the file COPYING.LIB. If > + not, see <https://www.gnu.org/licenses/>. */ > + > +/* Executed by tst-rtld-execfd through a binfmt_misc handler whose > + interpreter is the dynamic linker under test. Prints what an > + application observes; the parent compares it against a direct > + execution. */ > + > +#include <errno.h> > +#include <stdio.h> > +#include <sys/auxv.h> > + > +int > +main (int argc, char **argv) > +{ > + printf ("argc=%d\n", argc); > + for (int i = 0; i < argc; ++i) > + printf ("argv[%d]=%s\n", i, argv[i]); > + > + errno = 0; > + unsigned long int execfd = getauxval (AT_EXECFD); > + printf ("AT_EXECFD=%lu errno=%d\n", execfd, errno); > + > + const char *execfn = (const char *) getauxval (AT_EXECFN); > + printf ("AT_EXECFN=%s\n", execfn != NULL ? execfn : "(null)"); > + > + return 0; > +} > diff --git a/sysdeps/unix/sysv/linux/tst-rtld-execfd.c b/sysdeps/unix/sysv/linux/tst-rtld-execfd.c > new file mode 100644 > index 0000000000..3974a87b0d > --- /dev/null > +++ b/sysdeps/unix/sysv/linux/tst-rtld-execfd.c > @@ -0,0 +1,282 @@ > +/* Test that ld.so loads the main program from AT_EXECFD. > + 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; see the file COPYING.LIB. If > + not, see <https://www.gnu.org/licenses/>. */ > + > +/* Register the dynamic linker under test as a binfmt_misc extension > + handler with the 'O' (open-binary) flag in a private user and mount > + namespace, execute a helper through it, and verify that the helper > + was loaded from the AT_EXECFD descriptor: the application-visible > + argument vector is unchanged, the descriptor is not observable via > + getauxval, LD_TRACE_LOADED_OBJECTS works, and an execute-only (--x) > + copy - unopenable by path - still runs. > + > + Requires a kernel with per-user-namespace binfmt_misc mounts > + (Linux >= 6.7); reports UNSUPPORTED otherwise. */ > + > +#include <errno.h> > +#include <fcntl.h> > +#include <sched.h> > +#include <stdio.h> > +#include <stdlib.h> > +#include <string.h> > +#include <unistd.h> > +#include <sys/mount.h> > +#include <sys/stat.h> > +#include <sys/syscall.h> > +#include <linux/capability.h> > + > +#include <support/capture_subprocess.h> > +#include <support/check.h> > +#include <support/support.h> > +#include <support/temp_file.h> > +#include <support/xunistd.h> > + > +/* The binfmt_misc extension (filename suffix) the handler matches. */ > +#define EXT "tstexecfd" > + > +static char *binfmt_dir; > +static char *prog_copy; > +static char *libpath_env; > + > +static void > +write_string_to_file (const char *path, const char *str) > +{ > + int fd = xopen (path, O_WRONLY, 0); > + ssize_t len = strlen (str); > + if (write (fd, str, len) != len) > + FAIL_EXIT1 ("write (\"%s\", \"%s\"): %m", path, str); We have xwrite as well. This also could be a nice addition to support/ > + xclose (fd); > +} > + > +/* Create a user and mount namespace with uid/gid 0 mapped to the > + original ids. Unlike support_become_root, which identity-maps the > + original uid, the namespace's uid 0 must be mapped here: the > + binfmt_misc inodes are owned by it, and an unmapped owner cannot > + pass the permission (or capability) checks for writing to the > + register file. */ > +static bool > +become_ns_root (void) > +{ > + uid_t orig_uid = getuid (); > + gid_t orig_gid = getgid (); > + > + if (unshare (CLONE_NEWUSER | CLONE_NEWNS) != 0) > + return false; > + > + char *buf = xasprintf ("0 %llu 1\n", (unsigned long long) orig_uid); > + write_string_to_file ("/proc/self/uid_map", buf); > + free (buf); > + write_string_to_file ("/proc/self/setgroups", "deny\n"); > + buf = xasprintf ("0 %llu 1\n", (unsigned long long) orig_gid); > + write_string_to_file ("/proc/self/gid_map", buf); > + free (buf); > + > + /* Keep mount operations away from the host. */ > + if (mount ("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) > + return false; > + > + return getuid () == 0; > +} Should we add a support_become_ns_root to abstract it? > + > +static void > +copy_file (const char *from, const char *to, mode_t mode) > +{ > + int fd_from = xopen (from, O_RDONLY, 0); > + int fd_to = xopen (to, O_WRONLY | O_CREAT | O_TRUNC, 0700); > + char buf[65536]; > + ssize_t nread; > + while ((nread = read (fd_from, buf, sizeof (buf))) > 0) > + { > + ssize_t total = 0; > + while (total < nread) > + { > + ssize_t nwritten = write (fd_to, buf + total, nread - total); > + TEST_VERIFY_EXIT (nwritten > 0); > + total += nwritten; > + } > + } > + TEST_COMPARE (nread, 0); > + TEST_COMPARE (fchmod (fd_to, mode), 0); > + xclose (fd_from); > + xclose (fd_to); > +} We have support_copy_file for that. > + > +/* Drop the DAC-bypassing capabilities so that file permissions apply > + to this (namespace-root) process again. Cannot be undone. */ > +static void > +drop_dac_capabilities (void) > +{ > + struct __user_cap_header_struct header = > + { .version = _LINUX_CAPABILITY_VERSION_3, .pid = 0 }; > + struct __user_cap_data_struct data[2]; > + TEST_COMPARE (syscall (SYS_capget, &header, data), 0); > + data[0].effective &= ~((1u << CAP_DAC_OVERRIDE) | (1u << CAP_DAC_READ_SEARCH)); > + data[0].permitted &= ~((1u << CAP_DAC_OVERRIDE) | (1u << CAP_DAC_READ_SEARCH)); > + data[0].inheritable &= ~((1u << CAP_DAC_OVERRIDE) > + | (1u << CAP_DAC_READ_SEARCH)); > + TEST_COMPARE (syscall (SYS_capset, &header, data), 0); > +} > + > +static struct support_capture_subprocess > +run_prog_copy (const char *arg1, const char *arg2, bool trace) > +{ > + char *argv[] = { prog_copy, (char *) arg1, (char *) arg2, NULL }; > + char *envp[3] = { libpath_env, NULL, NULL }; > + if (trace) > + envp[1] = (char *) "LD_TRACE_LOADED_OBJECTS=1"; > + return support_capture_subprogram (prog_copy, argv, envp); > +} > + > +static int > +do_test (void) > +{ > + if (!become_ns_root ()) > + FAIL_UNSUPPORTED ("cannot create user+mount namespace with uid 0"); > + > + /* A binfmt_misc instance mounted in a user namespace is private to > + it: registrations neither affect nor require anything from the > + host (Linux >= 6.7). */ > + binfmt_dir = support_create_temp_directory ("tst-rtld-execfd-binfmt-"); > + if (mount ("binfmt_misc", binfmt_dir, "binfmt_misc", 0, NULL) != 0) > + { > + if (errno == ENODEV || errno == ENOENT || errno == ENOSYS > + || errno == EPERM || errno == EACCES || errno == EINVAL) > + FAIL_UNSUPPORTED ("cannot mount binfmt_misc: %m"); > + FAIL_EXIT1 ("mount binfmt_misc: %m"); > + } > + > + /* Register the dynamic linker under test as an extension handler > + with the 'O' flag, so the kernel keeps the executed binary open > + and passes it in AT_EXECFD. */ > + { > + char *reg = xasprintf (":tst-rtld-execfd:E::" EXT "::%s:O", > + support_objdir_elf_ldso); > + char *regpath = xasprintf ("%s/register", binfmt_dir); > + write_string_to_file (regpath, reg); > + free (regpath); > + free (reg); > + } > + > + /* The handler execs ld.so without options, so the helper must find > + the build-tree libraries through the environment. */ > + libpath_env = xasprintf ("LD_LIBRARY_PATH=%s:%s/elf", > + support_objdir_root, support_objdir_root); > + > + char *prog = xasprintf ("%s/elf/tst-rtld-execfd-prog", > + support_objdir_root); > + char *tmpdir = support_create_temp_directory ("tst-rtld-execfd-"); > + prog_copy = xasprintf ("%s/prog." EXT, tmpdir); > + copy_file (prog, prog_copy, 0755); > + add_temp_file (prog_copy); > + free (tmpdir); > + free (prog); > + > + /* Execute the helper through the handler. The application must > + observe exactly what a direct execution would produce: the argv > + the kernel spliced for the interpreter is consumed by ld.so as > + usual, and the descriptor is loaded from and neutralized. */ > + { > + struct support_capture_subprocess cap > + = run_prog_copy ("first-arg", "second-arg", false); > + support_capture_subprocess_check (&cap, "execfd", 0, sc_allow_stdout); > + char *expected = xasprintf ("argc=3\n" > + "argv[0]=%s\n" > + "argv[1]=first-arg\n" > + "argv[2]=second-arg\n" > + "AT_EXECFD=0 errno=%d\n" > + "AT_EXECFN=%s\n", > + prog_copy, ENOENT, prog_copy); > + TEST_COMPARE_STRING (cap.out.buffer, expected); > + free (expected); > + support_capture_subprocess_free (&cap); > + } > + > + /* LD_TRACE_LOADED_OBJECTS (ldd) must work for a descriptor-loaded > + main program. */ > + { > + struct support_capture_subprocess cap > + = run_prog_copy (NULL, NULL, true); > + support_capture_subprocess_check (&cap, "execfd trace", 0, > + sc_allow_stdout); > + TEST_VERIFY (strstr (cap.out.buffer, "libc.so") != NULL); > + support_capture_subprocess_free (&cap); > + } > + > + /* A program whose spliced path begins with "--" must be run, not > + mistaken for a dynamic-linker option: the kernel copies the > + execve() pathname into the interpreter's argument vector verbatim, > + so a program named like a loader option would otherwise divert or > + abort the loader. A relative path is required for the kernel to > + hand ld.so an argument starting with "--"; an absolute path always > + begins with "/". */ > + { > + const char *dashrel = "--library-path." EXT; > + char *dashdir = support_create_temp_directory ("tst-rtld-execfd-dash-"); > + char *dashabs = xasprintf ("%s/%s", dashdir, dashrel); > + copy_file (prog_copy, dashabs, 0755); > + add_temp_file (dashabs); > + > + xchdir (dashdir); > + char *argv[] = { (char *) dashrel, (char *) "tail", NULL }; > + char *envp[2] = { libpath_env, NULL }; > + struct support_capture_subprocess cap > + = support_capture_subprogram (dashrel, argv, envp); > + xchdir ("/"); > + support_capture_subprocess_check (&cap, "execfd dashname", 0, > + sc_allow_stdout); > + char *expected = xasprintf ("argc=2\n" > + "argv[0]=%s\n" > + "argv[1]=tail\n" > + "AT_EXECFD=0 errno=%d\n" > + "AT_EXECFN=%s\n", > + dashrel, ENOENT, dashrel); > + TEST_COMPARE_STRING (cap.out.buffer, expected); > + free (expected); > + support_capture_subprocess_free (&cap); > + free (dashabs); > + free (dashdir); > + } > + > + /* The headline capability: an execute-only binary cannot be opened > + by path, but execve() permits it and the descriptor the kernel > + passes is readable. Give up the DAC-override capabilities first, > + otherwise this (namespace-root) process could open it anyway. > + This must be the last subtest: the capabilities are gone. */ > + { > + TEST_COMPARE (chmod (prog_copy, 0111), 0); > + drop_dac_capabilities (); > + > + /* Control: the path really is unopenable now. This is what any > + path-based re-open in ld.so would run into. */ > + errno = 0; > + TEST_COMPARE (open (prog_copy, O_RDONLY), -1); > + TEST_COMPARE (errno, EACCES); > + > + struct support_capture_subprocess cap > + = run_prog_copy ("x-only", NULL, false); > + support_capture_subprocess_check (&cap, "execfd execute-only", 0, > + sc_allow_stdout); > + TEST_VERIFY (strstr (cap.out.buffer, "argv[1]=x-only") != NULL); > + TEST_VERIFY (strstr (cap.out.buffer, "AT_EXECFD=0 errno=2") != NULL); > + support_capture_subprocess_free (&cap); > + } > + > + umount2 (binfmt_dir, MNT_DETACH); I think we should move it either an atexit handler or add some support on test-container, otherwise if the test exits in an unexpected way the mount point won't be cleared out. > + return 0; > +} > + > +#include <support/test-driver.c> >