[PATCH 3/3] elf: add ld.so --program-fd
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu Jul 16 14:57:32 GMT 2026
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) <brauner@kernel.org>
> ---
> 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>
>
More information about the Libc-alpha
mailing list