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

Christian Brauner <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
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) <[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;
+	}
+
       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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.