[PATCH 3/3] elf: add ld.so --program-fd

Christian Brauner brauner@kernel.org
Wed Jul 15 10:13:05 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.

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;
+	}
+
       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>

-- 
2.53.0



More information about the Libc-alpha mailing list