Re: [PATCH 3/3] elf: add ld.so --program-fd
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: > Expose the AT_EXECFD loading path for explicit loader invocations: > > ld.so --program-fd NUMBER NAME [ARGS...] > > loads the main program from the inherited descriptor NUMBER; NAME is > still consumed as the program name argument and only names the program > (argument processing, --argv0 and everything else compose as usual). > FreeBSD's ld-elf.so.1 has the equivalent -f option. > > This makes running a program from a descriptor possible without any > kernel dispatch - e.g. executing a sealed memfd under a chosen loader > - and gives the descriptor-loading code deterministic test coverage on > kernels and CI setups where the binfmt_misc test is UNSUPPORTED. For a memfd it will make $ORIGIN become '/' (not a problem itself), should we add a comment somewhere? > > A descriptor given on the command line, unlike one installed by the > kernel, need not be open or positioned at the start, so reject closed > descriptors with a clean error (the lseek that rewinds the descriptor > doubles as the probe) and rewind before the ELF header check. The > --verify and --help code paths go through map_doit, which learns to > route around the path-based open when a descriptor is set. The > standard-descriptor evacuation introduced for AT_EXECFD now runs after > option parsing so it covers descriptors from either source. > > Signed-off-by: Christian Brauner (Amutable) <[email protected]> > --- > NEWS | 6 ++ > elf/Makefile | 4 ++ > elf/dl-load.c | 7 ++- > elf/dl-usage.c | 2 + > elf/rtld.c | 79 +++++++++++++++++++------- > elf/tst-rtld-program-fd-prog.c | 28 +++++++++ > elf/tst-rtld-program-fd.c | 126 +++++++++++++++++++++++++++++++++++++++++ > 7 files changed, 229 insertions(+), 23 deletions(-) > > diff --git a/NEWS b/NEWS > index cacd3f8be6..2988ffc14b 100644 > --- a/NEWS > +++ b/NEWS > @@ -18,6 +18,12 @@ Major new features: > handlers, and the descriptor refers to the file the kernel actually > access-checked, eliminating the re-open race. > > +* The dynamic linker accepts a new option --program-fd NUMBER when > + invoked as a command, loading the executable from the inherited > + descriptor NUMBER; the program name argument then only names the > + program. This is the explicit-invocation counterpart of AT_EXECFD > + (FreeBSD's ld-elf.so.1 has the equivalent -f option). > + > * A new tunable, glibc.elf.thp, is added to map read-only segments with > Transparent Huge Pages (THP) if THP isn't disable in kernel. When > glibc.elf.thp is set to 1, malloc uses the actual kernel THP mode > diff --git a/elf/Makefile b/elf/Makefile > index 01e77f2ca0..2acf8d9c71 100644 > --- a/elf/Makefile > +++ b/elf/Makefile > @@ -493,6 +493,7 @@ tests += \ > tst-rtld-no-malloc \ > tst-rtld-no-malloc-audit \ > tst-rtld-no-malloc-preload \ > + tst-rtld-program-fd \ > tst-rtld-run-static \ > tst-single_threaded \ > tst-single_threaded-pthread \ > @@ -595,6 +596,7 @@ tests-container += \ > > test-srcs = \ > tst-pathopt \ > + tst-rtld-program-fd-prog \ > tst-sprof-basic \ > # tests-srcs > > @@ -3248,6 +3250,8 @@ $(objpfx)tst-rtld-list-diagnostics.out: tst-rtld-list-diagnostics.py \ > > $@; \ > $(evaluate-test) > > +$(objpfx)tst-rtld-program-fd.out: $(objpfx)tst-rtld-program-fd-prog > + > $(objpfx)tst-rtld-run-static.out: $(objpfx)ldconfig > > $(objpfx)tst-dl_find_object.out: \ > diff --git a/elf/dl-load.c b/elf/dl-load.c > index 7c1794579b..8a79063a0f 100644 > --- a/elf/dl-load.c > +++ b/elf/dl-load.c > @@ -2258,8 +2258,11 @@ _dl_map_object_execfd (int fd, const char *name) > > /* The kernel hands over the descriptor with the file position at > zero, but an explicit loader invocation need not; the header check > - in open_verify reads sequentially. */ > - __lseek (fd, 0, SEEK_SET); > + in open_verify reads sequentially. This also rejects a descriptor > + that is not open at all. */ > + if (__lseek (fd, 0, SEEK_SET) == -1 && errno == EBADF) > + _dl_signal_error (EBADF, name, NULL, > + N_("cannot load main program from descriptor")); > > fd = open_verify (name, fd, &fb, NULL, 0, __RTLD_OPENEXEC, > &found_other_class, false); > diff --git a/elf/dl-usage.c b/elf/dl-usage.c > index a5bc1cb4ad..51db2355d3 100644 > --- a/elf/dl-usage.c > +++ b/elf/dl-usage.c > @@ -196,6 +196,8 @@ setting environment variables (which would be inherited by subprocesses).\n\ > --audit LIST use objects named in LIST as auditors\n\ > --preload LIST preload objects named in LIST\n\ > --argv0 STRING set argv[0] to STRING before running\n\ > + --program-fd FD load the executable from the inherited file\n\ > + descriptor FD; EXECUTABLE-FILE only names it\n\ > --list-tunables list all tunables with minimum and maximum values\n\ > --list-diagnostics list diagnostics information\n\ > --help display this help and exit\n\ > diff --git a/elf/rtld.c b/elf/rtld.c > index 9d29cc8f64..e6e160b63f 100644 > --- a/elf/rtld.c > +++ b/elf/rtld.c > @@ -19,6 +19,7 @@ > #include <errno.h> > #include <dlfcn.h> > #include <fcntl.h> > +#include <limits.h> > #include <stdbool.h> > #include <stdlib.h> > #include <string.h> > @@ -602,6 +603,9 @@ struct map_args > const char *str; > struct link_map *loader; > int mode; > + /* If not -1, map the main executable from this descriptor instead > + of opening STR (requires __RTLD_OPENEXEC in MODE). */ > + int execfd; > /* Return value of map_doit. */ > struct link_map *map; > }; > @@ -639,8 +643,11 @@ map_doit (void *a) > { > struct map_args *args = (struct map_args *) a; > int type = (args->mode == __RTLD_OPENEXEC) ? lt_executable : lt_library; > - args->map = _dl_map_object (args->loader, args->str, type, 0, > - args->mode, LM_ID_BASE); > + if (args->mode == __RTLD_OPENEXEC && args->execfd != -1) > + args->map = _dl_map_object_execfd (args->execfd, args->str); > + else > + args->map = _dl_map_object (args->loader, args->str, type, 0, > + args->mode, LM_ID_BASE); > } > > static void > @@ -791,6 +798,7 @@ do_preload (const char *fname, struct link_map *main_map, const char *where) > args.str = fname; > args.loader = main_map; > args.mode = __RTLD_SECURE; > + args.execfd = -1; > > unsigned int old_nloaded = GL(dl_ns)[LM_ID_BASE]._ns_nloaded; > > @@ -1450,25 +1458,6 @@ dl_main (const ElfW(Phdr) *phdr, > from_execfd = true; > break; > } > - > - /* Move the descriptor out of the standard range: it is closed > - once the program is mapped, and a secure process must not > - start with a silently closed standard descriptor. The > - standard descriptor check at startup ran while the descriptor > - still occupied the slot, so run it again once the slot is > - free. */ > - if (execfd >= 0 && execfd <= STDERR_FILENO) > - { > - int movedfd = __fcntl64_nocancel (execfd, F_DUPFD, > - STDERR_FILENO + 1); > - if (movedfd >= 0) > - { > - __close_nocancel (execfd); > - execfd = movedfd; > - if (__glibc_unlikely (__libc_enable_secure)) > - __libc_check_standard_fds (); > - } > - } > #endif > > /* When the kernel dispatches us as a binfmt interpreter, argv[1] > @@ -1536,6 +1525,22 @@ dl_main (const ElfW(Phdr) *phdr, > { > argv0 = _dl_argv[2]; > > + _dl_argc -= 2; > + _dl_argv += 2; > + } > + else if (! strcmp (_dl_argv[1], "--program-fd") && _dl_argc > 2) > + { > + /* Load the program from an inherited descriptor, like > + AT_EXECFD does; the program name argument only names > + it. Same semantics as FreeBSD's ld-elf.so.1 -f. */ > + char *endp; > + uint64_t fd = _dl_strtoul (_dl_argv[2], &endp); > + if (_dl_argv[2][0] == '-' || endp == _dl_argv[2] || *endp != '\0' > + || fd > INT_MAX) > + _dl_fatal_printf ("%s: invalid descriptor '%s' given to" > + " --program-fd\n", ld_so_name, _dl_argv[2]); > + execfd = fd; > + > _dl_argc -= 2; > _dl_argv += 2; > } > @@ -1591,6 +1596,31 @@ dl_main (const ElfW(Phdr) *phdr, > else > break; > > + /* Move the descriptor out of the standard range: it is closed > + once the program is mapped, and a secure process must not > + start with a silently closed standard descriptor. For > + AT_EXECFD, the standard descriptor check at startup ran while > + the descriptor still occupied the slot, so run it again once > + the slot is free. */ > + bool recheck_standard_fds = false; > + if (execfd >= 0 && execfd <= STDERR_FILENO) > + { > + int movedfd = __fcntl64_nocancel (execfd, F_DUPFD, > + STDERR_FILENO + 1); > + if (movedfd >= 0) > + { > + __close_nocancel (execfd); > + execfd = movedfd; > + if (__glibc_unlikely (__libc_enable_secure)) > + __libc_check_standard_fds (); > + } > + else if (__glibc_unlikely (__libc_enable_secure)) > + /* Cannot move it; it stays on the standard slot and is closed > + while mapping. Re-fill the slot below so a secure process > + does not start with it closed. */ > + recheck_standard_fds = true; > + } > + I think this fd dance here is redundant, currently with AT_SECURE and AT_EXECFD == 0 it does: fcntl(0,F_DUPFD,3) = 3 close(0) = 0 __libc_check_standard_fds \_ fcntl(0, F_GETFD) = -1 EBADF open("/dev/full", O_WRONLY) = 0 _dl_map_object_execfd \_ _dl_map_object_from_fd ... map reads/mmaps fd 3 ... close(3) = 0 __libc_check_standard_fds \_ fcntl(0, F_GETFD) = -1 EBADF open("/dev/full", O_WRONLY) = 0 I think if you just se the variable here: bool recheck_standard_fds = (execfd >= 0 && execfd <= STDERR_FILENO && __libc_enable_secure); the _dl_map_object_execfd will maps from the original 'execfd' and close it, and the __libc_check_standard_fds (called just after it) will open the required files: _dl_map_object_execfd \_ _dl_map_object_from_fd ... map reads/mmaps fd 0 ... close(0) = 0 __libc_check_standard_fds \_ fcntl(0, F_GETFD) = -1 EBADF open("/dev/full", O_WRONLY) = 0 And keep only one __libc_check_standard_fds call. I don't think it should care much for AT_EXECFD == 2, afaik kernel will always open it with O_RDONLY so any ld.so diagnostic will be lost anyway. > if (__glibc_unlikely (state.mode == rtld_mode_list_tunables)) > { > __tunables_print (); > @@ -1644,6 +1674,8 @@ dl_main (const ElfW(Phdr) *phdr, > args.str = rtld_progname; > args.loader = NULL; > args.mode = __RTLD_OPENEXEC; > + args.execfd = execfd; > + execfd_consumed = execfd != -1; > (void) _dl_catch_error (&objname, &err_str, &malloced, map_doit, > &args); > if (__glibc_unlikely (err_str != NULL)) > @@ -1676,6 +1708,11 @@ dl_main (const ElfW(Phdr) *phdr, > rtld_timer_stop (&load_time, start); > } > > + /* Fill the standard slot freed by closing an unevacuated program > + descriptor (see recheck_standard_fds above). */ > + if (__glibc_unlikely (recheck_standard_fds)) > + __libc_check_standard_fds (); > + > /* Now the map for the main executable is available. */ > main_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded; > > diff --git a/elf/tst-rtld-program-fd-prog.c b/elf/tst-rtld-program-fd-prog.c > new file mode 100644 > index 0000000000..183358f4aa > --- /dev/null > +++ b/elf/tst-rtld-program-fd-prog.c > @@ -0,0 +1,28 @@ > +/* Helper program for tst-rtld-program-fd: report the argument vector. > + 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/>. */ > + > +#include <stdio.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]); > + return 0; > +} > diff --git a/elf/tst-rtld-program-fd.c b/elf/tst-rtld-program-fd.c > new file mode 100644 > index 0000000000..0d3bba457e > --- /dev/null > +++ b/elf/tst-rtld-program-fd.c > @@ -0,0 +1,126 @@ > +/* Test the ld.so --program-fd option. > + 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/>. */ > + > +/* Run the dynamic linker with --program-fd on an inherited descriptor > + of the helper program: it must be loaded from the descriptor, with > + the program name argument only naming it. Exercises the same > + loading path as AT_EXECFD, without requiring kernel support. */ > + > +#include <errno.h> > +#include <fcntl.h> > +#include <stdio.h> > +#include <stdlib.h> > +#include <string.h> > +#include <unistd.h> > + > +#include <support/capture_subprocess.h> > +#include <support/check.h> > +#include <support/support.h> > +#include <support/xunistd.h> > + > +static int > +do_test (void) > +{ > + char *prog = xasprintf ("%s/elf/tst-rtld-program-fd-prog", > + support_objdir_root); > + char *libpath = xasprintf ("%s:%s/elf", support_objdir_root, > + support_objdir_root); > + > + /* No O_CLOEXEC: the descriptor must survive into ld.so. */ > + int fd = xopen (prog, O_RDONLY, 0); > + char *fdstr = xasprintf ("%d", fd); > + > + /* Plain use: the program comes from the descriptor, the name > + argument becomes argv[0]. */ > + { > + char *argv[] = > + { > + (char *) "ld.so", (char *) "--library-path", libpath, > + (char *) "--program-fd", fdstr, > + (char *) "displayed-name", (char *) "tail-arg", NULL > + }; > + struct support_capture_subprocess cap > + = support_capture_subprogram (support_objdir_elf_ldso, argv, NULL); > + support_capture_subprocess_check (&cap, "program-fd", 0, > + sc_allow_stdout); > + TEST_COMPARE_STRING (cap.out.buffer, > + "argc=2\n" > + "argv[0]=displayed-name\n" > + "argv[1]=tail-arg\n"); > + support_capture_subprocess_free (&cap); > + } > + > + /* Composes with --argv0. */ > + { > + xlseek (fd, 0, SEEK_SET); > + char *argv[] = > + { > + (char *) "ld.so", (char *) "--library-path", libpath, > + (char *) "--program-fd", fdstr, (char *) "--argv0", > + (char *) "overridden", (char *) "displayed-name", NULL > + }; > + struct support_capture_subprocess cap > + = support_capture_subprogram (support_objdir_elf_ldso, argv, NULL); > + support_capture_subprocess_check (&cap, "program-fd --argv0", 0, > + sc_allow_stdout); > + TEST_COMPARE_STRING (cap.out.buffer, > + "argc=1\n" > + "argv[0]=overridden\n"); > + support_capture_subprocess_free (&cap); > + } > + > + /* A closed descriptor must produce a clean error, not a crash. */ > + { > + char *argv[] = > + { > + (char *) "ld.so", (char *) "--program-fd", (char *) "977", > + (char *) "does-not-matter", NULL > + }; > + struct support_capture_subprocess cap > + = support_capture_subprogram (support_objdir_elf_ldso, argv, NULL); > + support_capture_subprocess_check (&cap, "program-fd bad fd", 127, > + sc_allow_stderr); > + TEST_VERIFY (strstr (cap.err.buffer, > + "cannot load main program from descriptor") > + != NULL); > + support_capture_subprocess_free (&cap); > + } > + > + /* A non-numeric argument must produce a clean error. */ > + { > + char *argv[] = > + { > + (char *) "ld.so", (char *) "--program-fd", (char *) "pear", > + (char *) "does-not-matter", NULL > + }; > + struct support_capture_subprocess cap > + = support_capture_subprogram (support_objdir_elf_ldso, argv, NULL); > + support_capture_subprocess_check (&cap, "program-fd non-numeric", 127, > + sc_allow_stderr); > + TEST_VERIFY (strstr (cap.err.buffer, "invalid descriptor") != NULL); > + support_capture_subprocess_free (&cap); > + } > + > + xclose (fd); > + free (fdstr); > + free (libpath); > + free (prog); > + return 0; > +} > + > +#include <support/test-driver.c> >