[PATCH v2 3/3] elf: add ld.so --program-fd
Christian Brauner
brauner@kernel.org
Thu Jul 16 14:55:31 GMT 2026
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.
The descriptor number is parsed with _dl_strtoul like the loader's
other numbers, but rejected unless it is a bare non-negative decimal in
range, so a signed or zero-padded argument cannot select an unintended
descriptor.
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 added with AT_EXECFD already covers a
descriptor from either source.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
NEWS | 6 ++
elf/Makefile | 4 ++
elf/dl-usage.c | 2 +
elf/rtld.c | 35 +++++++++++-
elf/tst-rtld-program-fd-prog.c | 28 +++++++++
elf/tst-rtld-program-fd.c | 127 +++++++++++++++++++++++++++++++++++++++++
6 files changed, 200 insertions(+), 2 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-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 858e945a04..084b804127 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>
@@ -603,6 +604,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;
};
@@ -640,8 +644,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
@@ -792,6 +799,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;
@@ -1517,6 +1525,28 @@ 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 as
+ FreeBSD's ld-elf.so.1 -f). Parse the descriptor with
+ _dl_strtoul as the loader parses its other numbers, then
+ reject what it would accept for a plain descriptor - a sign,
+ leading whitespace, a base prefix - and require a bare decimal
+ ("0" or [1-9][0-9]*), fully consumed and in range, like pldd's
+ pid check. */
+ const char *arg = _dl_argv[2];
+ char *endp;
+ uint64_t fd = _dl_strtoul (arg, &endp);
+ if (arg[0] < '0' || arg[0] > '9' || *endp != '\0'
+ || (arg[0] == '0' && arg[1] != '\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;
}
@@ -1650,6 +1680,7 @@ dl_main (const ElfW(Phdr) *phdr,
args.str = rtld_progname;
args.loader = NULL;
args.mode = __RTLD_OPENEXEC;
+ args.execfd = execfd;
(void) _dl_catch_error (&objname, &err_str, &malloced, map_doit,
&args);
if (__glibc_unlikely (err_str != NULL))
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..5a48fb3c9c
--- /dev/null
+++ b/elf/tst-rtld-program-fd.c
@@ -0,0 +1,127 @@
+/* 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. The full load in the previous subtest left
+ the shared descriptor's position undisturbed (open_verify preads),
+ so no rewind is needed here. */
+ {
+ 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>
--
2.53.0
More information about the Libc-alpha
mailing list