[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