[PATCH v3 0/4] elf: load the main program from AT_EXECFD

Christian Brauner <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
A Linux binfmt_misc handler can dispatch a program to a chosen dynamic
linker.  When the handler is registered with the 'O' (open binary) or 'C'
(credentials) flag, the kernel keeps the executed file open across the
dispatch.  It hands the descriptor to the interpreter in the AT_EXECFD
auxiliary vector entry.  This is the SVR4 contract.  AT_EXECFD is "the
file descriptor of the program to load", the alternative to AT_PHDR.
FreeBSD's rtld consumes it, and qemu-user has consumed it since 2013.

The GNU dynamic linker never has.  When such a handler dispatches to
ld.so, the descriptor is ignored.  It leaks into the new program.  ld.so
opens the executable again, this time by the path spliced into the
argument vector.

Opening the file again by path is racy, and often it is not possible at
all.  The file ld.so opens need not be the file the kernel checked and
executed.  A binary with execute permission but no read permission can be
run by execve().  It cannot be opened for reading by path, so ld.so
rejects it with EACCES, even though the descriptor it was handed is
readable.  Sometimes there is no path to open.  The program may be a
sealed memfd.

As Carlos said in [1] "I like where this is going because you can run
ld.so to execute an isolated AT_EXECFD application." which is a good way
to think about this.

Also, nixos wants to use binfmt_misc to execute elf binaries and will be
invoking the loader this way.  I also have a use-case for this in
systemd-homed in the future and I'm extending the abilities of
binfmt_misc a bit.  So I would really appreciate if we could support
this in the glibc loader.

This series makes ld.so load the main program from AT_EXECFD when ld.so
is the program the kernel loaded.  The object comes from the descriptor,
not from a path opened a second time.  The loaded file is exactly the one
execve() checked, so there is no race.  A binary with no read permission
runs.  A program with no accessible path runs.  Applications see no
difference. No kernel changes are needed.  The binfmt_misc 'O' and 'C'
flags have been around for a long time.  This works against current
kernels.

The series also adds the option "ld.so --program-fd N NAME".  This is the
explicit counterpart to AT_EXECFD.  It lets a program run from an
inherited descriptor, such as a sealed memfd, with no kernel dispatch at
all.

Next, following the discussion with Florian on the v1 thread, I am
working on a transparent mode for binfmt_misc.  The kernel labels
/proc/self/exe with the binary it access-checked instead of the
interpreter, leaves the argument vector unspliced, and raises a new
AT_FLAGS_PRESERVE_ARGV flag.  ld.so takes the program from AT_EXECFD,
names it from AT_EXECFN, and repairs /proc/self/auxv with one
unprivileged PR_SET_MM_MAP call.  The loader becomes invisible:
/proc/self/exe-based self-location and attaching a debugger behave as
for a direct execution.  That will be separate kernel and glibc
series. This series is the AT_EXECFD consumption they build on.

I've discussed this on mastodon in [2] with Carlos a little bit a couple
of days ago.

An LLM was used to get familiar with this particular part of the glibc
codebase and for review of the implementation. It's been a while so I
hope I still got the gist of contributions right.

Link: https://mastodon.social/deck/@[email protected]/116918469134997346 [1]
Link: https://mastodon.social/deck/@brauner/116913310306668229 [2]
Signed-off-by: Christian Brauner (Amutable) <[email protected]>
---
Changes in v3:
- Keep a standard-slot descriptor in place instead of moving it out of
  the way before mapping: consuming it closes the slot either way, so
  just re-run the secure standard-descriptor check once the program is
  mapped and the slot is free (Adhemerval).
- Factor the static-versus-dynamic classification shared by
  rtld_chain_load and rtld_execfd_check into one helper (Adhemerval).
- tst-rtld-execfd: use support_write_file_string and support_copy_file,
  add a support_become_ns_root helper next to support_become_root, and
  unmount the binfmt_misc instance from an atexit handler (Adhemerval).
- Test that --program-fd works with a descriptor positioned mid-file
  and leaves the shared file position undisturbed (Adhemerval).
- Document that $ORIGIN degrades to "/" for a path-less descriptor
  such as a sealed memfd, matching fexecve (Adhemerval).
- Split the --program-fd tests into their own patch.
- Link to v2: https://inbox.sourceware.org/libc-alpha/[email protected]

Changes in v2:
- Load the ELF header with pread so a shared descriptor's file
  position is neither relied upon nor disturbed (Florian).
- Fold the secure standard-descriptor recheck into patch 1 so no
  intermediate commit is exposed (Andreas Schwab).
- Reject closed descriptors via F_GETFD; parse --program-fd as a strict
  decimal, matching pldd's pid parsing.
- Add a generic dl-standard-fds.h wrapper so the loader links on Hurd.
- Harden tst-rtld-execfd (drop the capability bounding set) and use pread
  in the MIPS PT_MIPS_ABIFLAGS check.
- Link to v1: https://patch.msgid.link/[email protected]

---
Christian Brauner (4):
      elf: load the main program from AT_EXECFD when run as a binfmt interpreter
      elf: test AT_EXECFD consumption through a binfmt_misc 'O' handler
      elf: add ld.so --program-fd
      elf: test ld.so --program-fd

 NEWS                                           |  15 ++
 elf/Makefile                                   |   4 +
 elf/dl-load.c                                  |  67 ++++++-
 elf/dl-usage.c                                 |   2 +
 elf/rtld.c                                     | 160 ++++++++++++++--
 elf/tst-rtld-program-fd-prog.c                 |  28 +++
 elf/tst-rtld-program-fd.c                      | 150 +++++++++++++++
 support/Makefile                               |   1 +
 support/namespace.h                            |  12 ++
 support/support_become_ns_root.c               | 109 +++++++++++
 sysdeps/generic/dl-standard-fds.h              |  34 ++++
 sysdeps/generic/ldsodefs.h                     |   7 +
 sysdeps/mips/dl-machine-reject-phdr.h          |   4 +-
 sysdeps/unix/sysv/linux/Makefile               |   5 +
 sysdeps/unix/sysv/linux/dl-standard-fds.h      |  33 ++++
 sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c |  43 +++++
 sysdeps/unix/sysv/linux/tst-rtld-execfd.c      | 243 +++++++++++++++++++++++++
 17 files changed, 897 insertions(+), 20 deletions(-)
---
base-commit: 5396eb704531d9ede0388bffcc21b93bb661d404
change-id: 20260715-work-glibc-binfmt_misc-939266ac4824
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.