[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