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

Adhemerval Zanella Netto <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Organization Linaro
Message-ID <[email protected]>

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

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>
>
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.