From patchwork Fri Jul 17 11:11:58 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Christian Brauner X-Patchwork-Id: 139400 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 09A0D4BA23DF for ; Fri, 17 Jul 2026 11:13:00 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 09A0D4BA23DF Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=ee2srLPN X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from sea.source.kernel.org (sea.source.kernel.org [IPv6:2600:3c0a:e001:78e:0:1991:8:25]) by sourceware.org (Postfix) with ESMTPS id 392F84BA2E04 for ; Fri, 17 Jul 2026 11:12:11 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 392F84BA2E04 Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=kernel.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=kernel.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 392F84BA2E04 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2600:3c0a:e001:78e:0:1991:8:25 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286731; cv=none; b=fvp63X8pchq0fk2p4tJQjf8cbeNGbTSCidaliNb3CPDlt87XjcaGhrgwNQ10VxtQqXFri7DKf506t+WStBML1CpnbdHtoEDRu1rhlBV85YT8yRTzIMkpm5A1q0sFGUDQLPC7UmDRKBQYUY/fPsfbBr2hNer0Y0lb50p3JQHnRfo= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286731; c=relaxed/simple; bh=/YqlyHHGq25uhkMl7ByxNMzUeHkChI2MNXMgxB2g/Sg=; h=DKIM-Signature:From:Date:Subject:MIME-Version:Message-Id:To; b=Pd5WNHuEQ/WzTsOYHGvhzg/WiuCM8f139fFO/w+CD+8Erl+nc1Qd57bmtMRwS2b9mjBqW3l3FoVDTOteBPJ7LprjUOwx4MPkiLW1N8qvtZnFD82rfXZz5bF/QbIXGDkpXi/ffBGGI2vVZ7L0pKRoaP76tdPyFyZ6G1KTqsoftvw= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=ee2srLPN DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 392F84BA2E04 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) by sea.source.kernel.org (Postfix) with ESMTP id 93C754077B; Fri, 17 Jul 2026 11:12:10 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id D78721F000E9; Fri, 17 Jul 2026 11:12:08 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784286730; bh=10Ee5Sq6cvGtA8AShliBjJnUH5IEQOaD46NyWgNcXiU=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=ee2srLPNhJ+DQB4jGOfgEDHkzzr1UgbpCirVroFEoSxTTBQoW9Z2r4ZKjBn+61H/I SdNflPRdTuzFTbaDDV+pLCCOUj0ycKqndHWhcrOMQ1awATYLtRzpaHyImqNXkF6s6Y pdoDAr9OjnPbVXJI9mABOdBXWxHDGo24PB9YbJyKF7LGJd2iyZ4Z8d2sAuMGB3HTB9 R1dkwaw4oRjIk/xTIvxI0YJZ0BQhXJSP3/wtAnhTG5iCCm5TfB8n5y2sFJpsL1B+Hr FA9A/vXpQ6/KQS8hhR0JbSCa2DwZILdaUD7Q2R3S04iomd6cyKFwp1G2EXgKYu2ozW +hMFXNZ6Umn6w== From: Christian Brauner Date: Fri, 17 Jul 2026 13:11:58 +0200 Subject: [PATCH v3 1/4] elf: load the main program from AT_EXECFD when run as a binfmt interpreter MIME-Version: 1.0 Message-Id: <20260717-work-glibc-binfmt_misc-v3-1-45129bfb13fe@kernel.org> References: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> In-Reply-To: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> To: Florian Weimer , libc-alpha@sourceware.org Cc: Adhemerval Zanella Netto , Carlos O'Donell , "Christian Brauner (Amutable)" X-Mailer: b4 0.16-dev-4217c X-Developer-Signature: v=1; a=openpgp-sha256; l=20955; i=brauner@kernel.org; h=from:subject:message-id; bh=/YqlyHHGq25uhkMl7ByxNMzUeHkChI2MNXMgxB2g/Sg=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRF8bGWeCcXp6SWbO3e+lnuSYfNj4sFTJ+t5mywP7NTK 1f8HNP0jlIWBjEuBlkxRRaHdpNwueU8FZuNMjVg5rAygQxh4OIUgIkcSWL4Z582iVHziZvPFt0z zmfS97L+XfH2dOfpiX/0bZKzT64/5MbwT0Xl8KrJ9q+j92/fqqP3UF3F8hoHg9Ll0gnuV6ZfEVF dwgsA X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 X-Spam-Status: No, score=-10.1 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, SPF_HELO_NONE, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org A Linux binfmt_misc entry registered with the 'O' (open-binary) or 'C' (credentials) flag keeps the executed binary open across the handler dispatch and passes the descriptor to the interpreter in the AT_EXECFD auxiliary vector entry. The semantics are SVR4's: AT_EXECFD is the "file descriptor of program to load", the alternative to AT_PHDR. FreeBSD's rtld consumes it, qemu-user has consumed it since 2013 - and rtld never has ("We also do not handle AT_EXECFD even if it would be passed up"), so handlers dispatching to ld.so had the descriptor ignored, leaked into the application, and the program re-opened by path. Consume the descriptor, gated on the existing rtld_is_main detection. When ld.so is the main program and AT_EXECFD is present, load the main object from the descriptor via open_verify/_dl_map_object_from_fd instead of re-opening rtld_progname by path. Argument processing is entirely unchanged. Every producer of AT_EXECFD splices the interpreter and the binary path into the argument vector, so the program name argument is consumed exactly as before and the application-visible argument vector is identical. What changes is that the loaded file is now the very struct file that execve() access-checked (no re-open race), and that no path-based open happens at all, so execute-only (--x) binaries that execve() permits but ld.so previously failed to open with EACCES now run. The canonical name for $ORIGIN is derived from the descriptor by the existing __RTLD_OPENEXEC handling. The descriptor is consumed and closed while mapping, and the on-stack auxv entry is neutralized to AT_IGNORE so the program does not observe a dangling descriptor number. If the descriptor arrived on a standard descriptor slot (the kernel installs it on the lowest free one), consuming it closes that slot, so for secure processes the startup standard-descriptor check is re-run once the program is mapped, because it originally ran while the descriptor still occupied the slot. The descriptor is deliberately not moved out of the standard range before mapping: it is closed either way, so moving it would only change which descriptor number ends up closed. Chain loading is not attempted for a descriptor-loaded program. Re-executing the spliced path would run the process through the very binfmt handler that chose this dynamic linker again, and there may be no path that can be executed in the first place. Statically linked binaries are refused with a clear error instead, otherwise they would crash in-process again (bug 28648). The static-versus-dynamic classification is rtld_chain_load's, factored into a shared helper. Trace mode (LD_TRACE_LOADED_OBJECTS) never chain-loaded and behaves exactly as the explicit-loader invocation does today, including listing the dependencies of a descriptor-loaded dynamic executable. When rtld runs as the PT_INTERP of someone else's kernel-loaded main program (e.g. a dynamically linked qemu-user registered with 'O'), rtld_is_main is false and AT_EXECFD is addressed to that program, so nothing changes there. Signed-off-by: Christian Brauner (Amutable) --- NEWS | 9 +++ elf/dl-load.c | 65 +++++++++++++++- elf/rtld.c | 125 ++++++++++++++++++++++++++---- sysdeps/generic/dl-standard-fds.h | 34 ++++++++ sysdeps/generic/ldsodefs.h | 7 ++ sysdeps/mips/dl-machine-reject-phdr.h | 4 +- sysdeps/unix/sysv/linux/dl-standard-fds.h | 33 ++++++++ 7 files changed, 259 insertions(+), 18 deletions(-) diff --git a/NEWS b/NEWS index f9d90c5194..cacd3f8be6 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,15 @@ Version 2.44 Major new features: +* When the dynamic linker is executed as a binfmt interpreter (for + example through a Linux binfmt_misc entry registered with the 'O' + flag) and the kernel passes the executed program as an open + descriptor in the AT_EXECFD auxiliary vector entry, the dynamic + linker now loads the program from that descriptor instead of + re-opening it by path. Execute-only (--x) binaries work under such + handlers, and the descriptor refers to the file the kernel actually + access-checked, eliminating the re-open race. + * 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/dl-load.c b/elf/dl-load.c index 95404adae9..f2f14dd6f3 100644 --- a/elf/dl-load.c +++ b/elf/dl-load.c @@ -1625,11 +1625,14 @@ open_verify (const char *name, int fd, __set_errno (0); fbp->len = 0; assert (sizeof (fbp->buf) > sizeof (ElfW(Ehdr))); - /* Read in the header. */ + /* Read the header with pread: an executable descriptor handed to + the loader may be shared, so its file position must be neither + relied upon nor disturbed. */ do { - ssize_t retlen = __read_nocancel (fd, fbp->buf + fbp->len, - sizeof (fbp->buf) - fbp->len); + ssize_t retlen = __pread64_nocancel (fd, fbp->buf + fbp->len, + sizeof (fbp->buf) - fbp->len, + fbp->len); if (retlen <= 0) break; fbp->len += retlen; @@ -2241,6 +2244,62 @@ _dl_map_object (struct link_map *loader, const char *name, return _dl_map_new_object (loader, name, type, trace_mode, mode, nsid); } +/* Map in the main executable, already opened on FD. The descriptor + comes from the kernel (AT_EXECFD, from a binfmt interpreter dispatch + that kept the executed binary open) or from an explicit loader + invocation. NAME is the name the program is known by and is only + used for diagnostics; the canonical name used for $ORIGIN is derived + from the descriptor itself (__RTLD_OPENEXEC). There may be no path + the program could be opened by: the descriptor is readable even for + an execute-only binary, and it refers to the very file the kernel + access-checked, so no path re-open takes its place. */ +struct link_map * +_dl_map_object_execfd (int fd, const char *name) +{ + struct filebuf fb; + bool found_other_class = false; + + /* open_verify reads with pread and ignores the file position, so only + a descriptor that is not open at all needs rejecting (an explicit + loader invocation can be handed one). */ + if (__fcntl64_nocancel (fd, F_GETFD) == -1) + _dl_signal_error (errno, name, NULL, + N_("cannot load main program from descriptor")); + + fd = open_verify (name, fd, &fb, NULL, 0, __RTLD_OPENEXEC, + &found_other_class, false); + if (__glibc_unlikely (fd == -1)) + { + if (found_other_class) + _dl_signal_error (0, name, NULL, + ELFW(CLASS) == ELFCLASS32 + ? N_("wrong ELF class: ELFCLASS64") + : N_("wrong ELF class: ELFCLASS32")); + else if (errno == ENOENT) + /* The descriptor is open (checked above), so ENOENT here is + open_verify's report of an incompatible ELF machine, not a + missing file. */ + _dl_signal_error (0, name, NULL, + N_("cannot load main program from descriptor:" + " incompatible ELF machine")); + else + _dl_signal_error (errno, name, NULL, + N_("cannot load main program from descriptor")); + } + + char *realname = __strdup (name); + if (realname == NULL) + { + __close_nocancel (fd); + _dl_signal_error (ENOMEM, name, NULL, + N_("cannot allocate name record")); + } + + return _dl_map_object_from_fd (name, NULL, fd, &fb, realname, NULL, + lt_executable, __RTLD_OPENEXEC, + __libc_stack_end, LM_ID_BASE); +} + struct add_path_state { diff --git a/elf/rtld.c b/elf/rtld.c index e5ba71fef1..3d383ae3b9 100644 --- a/elf/rtld.c +++ b/elf/rtld.c @@ -54,6 +54,7 @@ #include #include #include +#include #include @@ -1044,10 +1045,11 @@ load_audit_modules (struct link_map *main_map, struct audit_list *audit_list) } } -/* Check if the executable is not actually dynamically linked, and - invoke it directly in that case. */ -static void -rtld_chain_load (struct link_map *main_map, char *argv0) +/* Diagnose the dynamic loader run against itself (fatal), and return + whether the main executable is dynamically linked: it has DT_NEEDED + dependencies or a program interpreter. */ +static bool +rtld_main_map_is_dynamic (struct link_map *main_map) { /* The dynamic loader run against itself. */ const char *rtld_soname = l_soname (&_dl_rtld_map); @@ -1058,14 +1060,26 @@ rtld_chain_load (struct link_map *main_map, char *argv0) /* With DT_NEEDED dependencies, the executable is dynamically linked. */ if (__glibc_unlikely (main_map->l_info[DT_NEEDED] != NULL)) - return; + return true; /* If the executable has program interpreter, it is dynamically linked. */ for (size_t i = 0; i < main_map->l_phnum; ++i) if (main_map->l_phdr[i].p_type == PT_INTERP) - return; + return true; + + return false; +} +/* Check if the executable is not actually dynamically linked, and + invoke it directly in that case. */ +static void +rtld_chain_load (struct link_map *main_map, char *argv0) +{ + if (rtld_main_map_is_dynamic (main_map)) + return; + + const char *rtld_soname = l_soname (&_dl_rtld_map); const char *pathname = _dl_argv[0]; if (argv0 != NULL) _dl_argv[0] = argv0; @@ -1079,6 +1093,20 @@ rtld_chain_load (struct link_map *main_map, char *argv0) rtld_soname, pathname, errcode); } +/* The main executable was loaded from a descriptor and cannot be + chain-loaded through execve: there may be no path it can be executed + by, and re-executing it would run the process through the binfmt + handler that chose this dynamic linker in the first place, again. + Refuse the cases rtld_chain_load would have chained instead of + crashing on them later (bug 28648). */ +static void +rtld_execfd_check (struct link_map *main_map) +{ + if (!rtld_main_map_is_dynamic (main_map)) + _dl_fatal_printf ("%s: cannot execute a statically linked binary" + " from a descriptor\n", l_soname (&_dl_rtld_map)); +} + /* Called to complete the initialization of the link map for the main executable. Returns true if there is a PT_INTERP segment. */ static bool @@ -1382,16 +1410,50 @@ dl_main (const ElfW(Phdr) *phdr, like that. We just load it and use its entry point; we don't pay attention to its PT_INTERP command (we are the interpreter ourselves). This is an easy way to test a new ld.so before - installing it. */ + installing it. + + This also happens when the kernel executes us on behalf of a + binfmt interpreter dispatch (binfmt_misc): the handler splices + our path and the program's path into the argument vector, so + the arguments are processed just the same. If the handler + also kept the program open ('O' and 'C' entries), AT_EXECFD + carries the descriptor and the program is loaded from it + instead of re-opening the path (see below). */ rtld_is_main = true; char *argv0 = NULL; char **orig_argv = _dl_argv; + int execfd = -1; + /* True if the kernel dispatched us as a binfmt interpreter + (AT_EXECFD is present). */ + bool from_execfd = false; /* Note the place where the dynamic linker actually came from. */ _dl_rtld_map.l_name = rtld_progname; - while (_dl_argc > 1) +#ifdef HAVE_AUX_VECTOR + /* A binfmt interpreter dispatch that keeps the executed binary + open passes the descriptor in AT_EXECFD. Load the program + from it: the path spliced into the argument vector may not be + openable again (execute-only binaries), and the descriptor + refers to the very file the kernel access-checked, so no + re-open races against it. The raw vector must be scanned + because a valid descriptor 0 and an absent entry cannot be + told apart in the parsed values. */ + for (ElfW(auxv_t) *av = auxv; av->a_type != AT_NULL; av++) + if (av->a_type == AT_EXECFD) + { + execfd = av->a_un.a_val; + from_execfd = true; + break; + } +#endif + + /* When the kernel dispatches us as a binfmt interpreter, argv[1] + is the spliced program path, not a loader option. It is + attacker-controlled and may begin with "--" (e.g. a program + named "--preload"), so do not parse it as an option. */ + while (!from_execfd && _dl_argc > 1) if (! strcmp (_dl_argv[1], "--list")) { if (state.mode != rtld_mode_help) @@ -1507,6 +1569,17 @@ dl_main (const ElfW(Phdr) *phdr, else break; + /* A descriptor on a standard slot is consumed and closed while + the program is mapped below, and a secure process must not run + with a silently closed standard descriptor: the startup check + ran while the descriptor still occupied the slot. Re-run it + once the program is mapped and the slot is free. Moving the + descriptor out of the standard range instead would only change + which descriptor number ends up closed. */ + bool recheck_standard_fds + = (execfd >= 0 && execfd <= STDERR_FILENO + && __glibc_unlikely (__libc_enable_secure)); + if (__glibc_unlikely (state.mode == rtld_mode_list_tunables)) { __tunables_print (); @@ -1581,16 +1654,29 @@ dl_main (const ElfW(Phdr) *phdr, #ifdef HAVE_THP _dl_get_thp_config (); #endif - _dl_map_object (NULL, rtld_progname, lt_executable, 0, - __RTLD_OPENEXEC, LM_ID_BASE); + if (execfd != -1) + _dl_map_object_execfd (execfd, rtld_progname); + else + _dl_map_object (NULL, rtld_progname, lt_executable, 0, + __RTLD_OPENEXEC, LM_ID_BASE); rtld_timer_stop (&load_time, start); } + /* Fill the standard slot freed by closing the program + descriptor (see recheck_standard_fds above). */ + if (__glibc_unlikely (recheck_standard_fds)) + _dl_recheck_standard_fds (); + /* Now the map for the main executable is available. */ main_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded; if (__glibc_likely (state.mode == rtld_mode_normal)) - rtld_chain_load (main_map, argv0); + { + if (execfd != -1) + rtld_execfd_check (main_map); + else + rtld_chain_load (main_map, argv0); + } phdr = main_map->l_phdr; phnum = main_map->l_phnum; @@ -1622,6 +1708,13 @@ dl_main (const ElfW(Phdr) *phdr, case AT_EXECFN: av->a_un.a_val = (uintptr_t) _dl_argv[0]; break; + case AT_EXECFD: + /* An AT_EXECFD entry is present only when the kernel + dispatched us, and the program is then always loaded and + closed from the descriptor, so do not leave a dangling + number behind. */ + av->a_type = AT_IGNORE; + break; } #endif @@ -1650,8 +1743,14 @@ dl_main (const ElfW(Phdr) *phdr, /* At this point we are in a bit of trouble. We would have to fill in the values for l_dev and l_ino. But in general we - do not know where the file is. We also do not handle AT_EXECFD - even if it would be passed up. + do not know where the file is. AT_EXECFD is deliberately not + consumed here: it carries the file the kernel did *not* load + and is addressed to the main program the kernel *did* load - + the registered binfmt interpreter (e.g. a dynamically linked + qemu-user), whose PT_INTERP we merely are. Only when the + registered interpreter is ld.so itself is ld.so that main + program, and then the descriptor is consumed in the + rtld-as-command branch above. We leave the values here defined to 0. This is normally no problem as the program code itself is normally no shared diff --git a/sysdeps/generic/dl-standard-fds.h b/sysdeps/generic/dl-standard-fds.h new file mode 100644 index 0000000000..d6c3589ffe --- /dev/null +++ b/sysdeps/generic/dl-standard-fds.h @@ -0,0 +1,34 @@ +/* Re-check the standard file descriptors in the dynamic linker. Generic. + 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 . */ + +#ifndef _DL_STANDARD_FDS_H +#define _DL_STANDARD_FDS_H + +/* Re-run the startup standard-descriptor check after the dynamic + linker closed a descriptor in the standard range: a secure process + must not start with a silently closed standard descriptor. The + generic version is a no-op: ports without the check in the dynamic + linker heal the standard descriptors during libc initialization + instead (the Hurd does from its _hurd_fd_subinit hook, and does not + define __libc_check_standard_fds for shared builds). */ +static inline void +_dl_recheck_standard_fds (void) +{ +} + +#endif diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h index f94247ad9f..b97282aaac 100644 --- a/sysdeps/generic/ldsodefs.h +++ b/sysdeps/generic/ldsodefs.h @@ -933,6 +933,13 @@ struct link_map *_dl_map_new_object (struct link_map *loader, int type, int trace_mode, int mode, Lmid_t nsid) attribute_hidden; +/* Map in the main executable from the already-open descriptor FD + (AT_EXECFD or an explicit loader invocation). NAME is the name the + program is known by; the canonical name used for $ORIGIN is derived + from FD. FD is consumed. */ +extern struct link_map *_dl_map_object_execfd (int fd, const char *name) + attribute_hidden; + /* Call _dl_map_object on the dependencies of MAP, and set up MAP->l_searchlist. PRELOADS points to a vector of NPRELOADS previously diff --git a/sysdeps/mips/dl-machine-reject-phdr.h b/sysdeps/mips/dl-machine-reject-phdr.h index e64494c198..88d0a3cd6a 100644 --- a/sysdeps/mips/dl-machine-reject-phdr.h +++ b/sysdeps/mips/dl-machine-reject-phdr.h @@ -209,8 +209,8 @@ elf_machine_reject_phdr_p (const struct dl_machine_phdr_info *info, if (ph->p_filesz < size) REJECT (" contains malformed PT_MIPS_ABIFLAGS\n"); - __lseek (fd, ph->p_offset, SEEK_SET); - if (__libc_read (fd, (void *) &mips_abiflags, size) != size) + if (__pread64_nocancel (fd, (void *) &mips_abiflags, size, + ph->p_offset) != size) REJECT (" unable to read PT_MIPS_ABIFLAGS\n"); if (__glibc_unlikely (mips_abiflags.flags2 != 0)) diff --git a/sysdeps/unix/sysv/linux/dl-standard-fds.h b/sysdeps/unix/sysv/linux/dl-standard-fds.h new file mode 100644 index 0000000000..207af0ab0b --- /dev/null +++ b/sysdeps/unix/sysv/linux/dl-standard-fds.h @@ -0,0 +1,33 @@ +/* Re-check the standard file descriptors in the dynamic linker. Linux. + 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 . */ + +#ifndef _DL_STANDARD_FDS_H +#define _DL_STANDARD_FDS_H + +#include + +/* Linux runs the startup standard-descriptor check inside the dynamic + linker (dl-sysdep.c), so the re-check after closing a program + descriptor runs there as well. */ +static inline void +_dl_recheck_standard_fds (void) +{ + __libc_check_standard_fds (); +} + +#endif From patchwork Fri Jul 17 11:11:59 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Christian Brauner X-Patchwork-Id: 139402 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id B60964BA23DA for ; Fri, 17 Jul 2026 11:13:14 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org B60964BA23DA Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=T7bnxqaS X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from tor.source.kernel.org (tor.source.kernel.org [172.105.4.254]) by sourceware.org (Postfix) with ESMTPS id 218724BA2E0A for ; Fri, 17 Jul 2026 11:12:13 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 218724BA2E0A Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=kernel.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=kernel.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 218724BA2E0A Authentication-Results: sourceware.org; arc=none smtp.remote-ip=172.105.4.254 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286733; cv=none; b=LF3NzqgjVnOgXeoO/sYZ5DTcxkwIpuZFNiKm9imPFZQOF39qDXCb98fu2Ps++J4fJ6XEhDEYaSsCTiHbEb8ym+tTjltqaXlH1UBb+IHf3eHgNiptZet9M7xzb4nwr7fsLJQdsZ6OEy3uHF82joHDZqOBak822CUO2uP6bAFFOVg= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286733; c=relaxed/simple; bh=9Zr9AX/FQ5jCJYkuYEZsgFueWvK9EX/MZLYnEzXXP80=; h=DKIM-Signature:From:Date:Subject:MIME-Version:Message-Id:To; b=sMwIl+r+GHixeXsfOLR9ewogIvZvP+UMxtIJ0sF1gFL/qGadf+I7lZjZQHfnc3Tzuh9DEsraUOTIBevWdsM40OwVnD2/WyAYFnEAsgkdDJWPF7THKI5ypU8uUwPV2nGdjp2SyiT0Q8+tUuXM6fXE2o6zx5JBYX3WKuqvWVi6D0g= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=T7bnxqaS DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 218724BA2E0A Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) by tor.source.kernel.org (Postfix) with ESMTP id A9E7E60204; Fri, 17 Jul 2026 11:12:12 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id EEDCD1F000E9; Fri, 17 Jul 2026 11:12:10 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784286732; bh=4ttZLw0gLLEk7SEgyXT6SoLAx2y4VI+I93frKnX6xlo=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=T7bnxqaSc81ie/hdVFThq5iHUwoTGEZNP7txDrCZavQ1n+jd19ssDVXf4irR72tEi 5rsd4/6A3F1Lu2ht2YmGRp7ZIcl3Sl5zYUUOiDdD12/UGBPQS0qfCGdmRGJs1iG7XV rbkXoQRstNCvMd7MbvAWORvEIcJOD6vhs21YGDQxSAV1osa62h4FrhY3CbqVEjeDBH bUrRrSeY8JQkfrT8NfzP6Ioz9jo25oKqE1VJ26+rX+wxCqTLQGkoxt08CW/SuzSrpn EfO3yhIsoZiYcasYyk5ZWOs8w0ZUw7zqEXLWL6OB2Gvq6r98mo9X/6Oxnwc6zFMoQn RRE8iu3xwQmPA== From: Christian Brauner Date: Fri, 17 Jul 2026 13:11:59 +0200 Subject: [PATCH v3 2/4] elf: test AT_EXECFD consumption through a binfmt_misc 'O' handler MIME-Version: 1.0 Message-Id: <20260717-work-glibc-binfmt_misc-v3-2-45129bfb13fe@kernel.org> References: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> In-Reply-To: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> To: Florian Weimer , libc-alpha@sourceware.org Cc: Adhemerval Zanella Netto , Carlos O'Donell , "Christian Brauner (Amutable)" X-Mailer: b4 0.16-dev-4217c X-Developer-Signature: v=1; a=openpgp-sha256; l=20425; i=brauner@kernel.org; h=from:subject:message-id; bh=9Zr9AX/FQ5jCJYkuYEZsgFueWvK9EX/MZLYnEzXXP80=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRF8bF+ZH/Nc0elrEPNY7bIw5Lalcvtair2Wj+eyL2Qc eLy1j1dHaUsDGJcDLJiiiwO7Sbhcst5KjYbZWrAzGFlAhnCwMUpABPZyMjI0LwkjJt/zYn4SYXK ES9MJzmkvw5Y2ynhf39RaJ5+knfXbkaGzzGvE3YHPVtdkiolsW+2uM7/5YdaDy/xVDazfs38n5m RHQA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 X-Spam-Status: No, score=-10.1 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, SPF_HELO_NONE, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org Register the dynamic linker under test as a binfmt_misc extension handler with the 'O' (open-binary) flag inside a private user and mount namespace - binfmt_misc instances are per-user-namespace since Linux 6.7, so nothing leaks to the host and the test is parallel-safe. Kernels without sandboxed binfmt_misc mounts report UNSUPPORTED. The helper executed through the handler verifies the contract: - the application-visible argument vector is exactly what a direct execution produces (the splice is consumed by ld.so as usual), - getauxval (AT_EXECFD) reports the entry as absent: the descriptor was consumed, closed, and neutralized to AT_IGNORE, - AT_EXECFN is the original path, - LD_TRACE_LOADED_OBJECTS lists the dependencies of the descriptor-loaded main program, - an execute-only (--x) copy still runs after the test sheds CAP_DAC_OVERRIDE/CAP_DAC_READ_SEARCH, while a path open fails with EACCES - proving the program really is loaded from the descriptor and not re-opened by path. The namespace setup maps uid/gid 0 (unshare -r style) rather than using support_become_root, which identity-maps the original uid: the binfmt_misc inodes are owned by the namespace's uid 0, and an unmapped owner cannot pass the permission checks for the register file. It is provided as a new support_become_ns_root helper next to support_become_root. The binfmt_misc instance is unmounted from an atexit handler so a failing subtest does not leave the mount pinning the temporary directory. Signed-off-by: Christian Brauner (Amutable) --- support/Makefile | 1 + support/namespace.h | 12 ++ support/support_become_ns_root.c | 109 +++++++++++ sysdeps/unix/sysv/linux/Makefile | 5 + sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c | 43 +++++ sysdeps/unix/sysv/linux/tst-rtld-execfd.c | 243 +++++++++++++++++++++++++ 6 files changed, 413 insertions(+) diff --git a/support/Makefile b/support/Makefile index 87eeb8199f..3790c909f5 100644 --- a/support/Makefile +++ b/support/Makefile @@ -51,6 +51,7 @@ libsupport-routines = \ resolv_test \ set_fortify_handler \ support-open-dev-null-range \ + support_become_ns_root \ support_become_root \ support_can_chroot \ support_capture_subprocess \ diff --git a/support/namespace.h b/support/namespace.h index f4b29a180f..9a0e38faf6 100644 --- a/support/namespace.h +++ b/support/namespace.h @@ -35,6 +35,18 @@ __BEGIN_DECLS single-threaded processes. */ bool support_become_root (void); +/* Attempts to become root (UID and GID 0) in a new user namespace, + with a new mount namespace in which mount operations do not affect + the host (/ is marked private). Unlike support_become_root, which + identity-maps the original UID, the namespace's UID 0 is mapped to + the original UID: kernel objects created in the namespace are owned + by its UID 0, and an unmapped owner cannot pass permission checks + on them. Return true if the namespace's root user could be + attained. Print diagnostics to standard output. The note on + multi-threaded processes for support_become_root applies here as + well. */ +bool support_become_ns_root (void); + /* Return true if this process can perform a chroot operation. In general, this is only possible if support_become_root has been called. Note that the actual test is performed in a subprocess, diff --git a/support/support_become_ns_root.c b/support/support_become_ns_root.c new file mode 100644 index 0000000000..1b05fdc712 --- /dev/null +++ b/support/support_become_ns_root.c @@ -0,0 +1,109 @@ +/* Become root inside a new user namespace. + 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; if not, see + . */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CLONE_NEWNS +# include +#endif /* CLONE_NEWNS */ + +#if defined CLONE_NEWUSER && defined CLONE_NEWNS +/* Write STR to PATH. Failure is left to the caller to report, so + that an unsupported kernel results in a false return from + support_become_ns_root, not a test failure. */ +static bool +write_string_to_file (const char *path, const char *str) +{ + int fd = open64 (path, O_WRONLY); + if (fd < 0) + return false; + ssize_t len = strlen (str); + bool ok = write (fd, str, len) == len; + xclose (fd); + return ok; +} +#endif /* CLONE_NEWUSER && CLONE_NEWNS */ + +bool +support_become_ns_root (void) +{ +#if defined CLONE_NEWUSER && defined CLONE_NEWNS + uid_t original_uid = getuid (); + gid_t original_gid = getgid (); + + if (unshare (CLONE_NEWUSER | CLONE_NEWNS) != 0) + { + printf ("warning: unshare (CLONE_NEWUSER | CLONE_NEWNS) failed: %m\n"); + return false; + } + + /* Map the namespace's UID 0 to the original UID. Unlike the + identity mapping support_become_root sets up, this makes the + process the owner of kernel objects created in the namespace, + which are owned by its UID 0. */ + char buf[100]; + int ret = snprintf (buf, sizeof (buf), "0 %llu 1\n", + (unsigned long long) original_uid); + TEST_VERIFY_EXIT (ret < sizeof (buf)); + if (!write_string_to_file ("/proc/self/uid_map", buf)) + { + printf ("warning: writing to /proc/self/uid_map failed: %m\n"); + return false; + } + + /* Linux 3.19 introduced the setgroups file. "deny" must be written + to it before gid_map becomes writable. */ + if (!write_string_to_file ("/proc/self/setgroups", "deny\n") + && errno != ENOENT) + { + printf ("warning: writing to /proc/self/setgroups failed: %m\n"); + return false; + } + + /* Now map the namespace's GID 0, like the UID. */ + ret = snprintf (buf, sizeof (buf), "0 %llu 1\n", + (unsigned long long) original_gid); + TEST_VERIFY_EXIT (ret < sizeof (buf)); + if (!write_string_to_file ("/proc/self/gid_map", buf)) + { + printf ("warning: writing to /proc/self/gid_map failed: %m\n"); + return false; + } + + /* On some systems, / is marked as MS_SHARED, which means that + mounts within the namespace leak to the rest of the system, + which is not what we want. */ + if (mount ("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) + { + printf ("warning: making the mount namespace private failed: %m\n"); + return false; + } + + return getuid () == 0; +#else + return false; +#endif /* CLONE_NEWUSER && CLONE_NEWNS */ +} diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile index 56b160e253..d1abf2b563 100644 --- a/sysdeps/unix/sysv/linux/Makefile +++ b/sysdeps/unix/sysv/linux/Makefile @@ -697,6 +697,7 @@ $(objpfx)pldd: $(objpfx)xmalloc.o tests += \ tst-rseq-tls-range \ tst-rseq-tls-range-4096 \ + tst-rtld-execfd \ tst-thp-1 \ tst-thp-1-pde \ tst-thp-1-static \ @@ -707,6 +708,10 @@ tests-static += \ tst-rseq-tls-range-static \ tst-thp-1-static \ # tests-static +test-srcs += \ + tst-rtld-execfd-prog \ +# test-srcs +$(objpfx)tst-rtld-execfd.out: $(objpfx)tst-rtld-execfd-prog modules-names += \ tst-rseq-tls-range-mod \ tst-thp-size-mod \ diff --git a/sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c b/sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c new file mode 100644 index 0000000000..e209279255 --- /dev/null +++ b/sysdeps/unix/sysv/linux/tst-rtld-execfd-prog.c @@ -0,0 +1,43 @@ +/* Helper program for tst-rtld-execfd: report argv and AT_EXECFD state. + 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 . */ + +/* Executed by tst-rtld-execfd through a binfmt_misc handler whose + interpreter is the dynamic linker under test. Prints what an + application observes; the parent compares it against a direct + execution. */ + +#include +#include +#include + +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]); + + errno = 0; + unsigned long int execfd = getauxval (AT_EXECFD); + printf ("AT_EXECFD=%lu errno=%d\n", execfd, errno); + + const char *execfn = (const char *) getauxval (AT_EXECFN); + printf ("AT_EXECFN=%s\n", execfn != NULL ? execfn : "(null)"); + + return 0; +} diff --git a/sysdeps/unix/sysv/linux/tst-rtld-execfd.c b/sysdeps/unix/sysv/linux/tst-rtld-execfd.c new file mode 100644 index 0000000000..4402a351c7 --- /dev/null +++ b/sysdeps/unix/sysv/linux/tst-rtld-execfd.c @@ -0,0 +1,243 @@ +/* Test that ld.so loads the main program from AT_EXECFD. + 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 . */ + +/* Register the dynamic linker under test as a binfmt_misc extension + handler with the 'O' (open-binary) flag in a private user and mount + namespace, execute a helper through it, and verify that the helper + was loaded from the AT_EXECFD descriptor: the application-visible + argument vector is unchanged, the descriptor is not observable via + getauxval, LD_TRACE_LOADED_OBJECTS works, and an execute-only (--x) + copy - unopenable by path - still runs. + + Requires a kernel with per-user-namespace binfmt_misc mounts + (Linux >= 6.7); reports UNSUPPORTED otherwise. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* The binfmt_misc extension (filename suffix) the handler matches. */ +#define EXT "tstexecfd" + +static char *binfmt_dir; +static char *prog_copy; +static char *libpath_env; + +/* Unmount the binfmt_misc instance. Registered with atexit so that a + failing subtest does not leave the mount pinning the temporary + directory; the temporary files are deleted after this runs (atexit + handlers run in reverse registration order). */ +static void +unmount_binfmt (void) +{ + umount2 (binfmt_dir, MNT_DETACH); +} + +/* Drop the DAC-bypassing capabilities so that file permissions apply + to this (namespace-root) process again. Cannot be undone. */ +static void +drop_dac_capabilities (void) +{ + struct __user_cap_header_struct header = + { .version = _LINUX_CAPABILITY_VERSION_3, .pid = 0 }; + struct __user_cap_data_struct data[2]; + TEST_COMPARE (syscall (SYS_capget, &header, data), 0); + data[0].effective &= ~((1u << CAP_DAC_OVERRIDE) | (1u << CAP_DAC_READ_SEARCH)); + data[0].permitted &= ~((1u << CAP_DAC_OVERRIDE) | (1u << CAP_DAC_READ_SEARCH)); + data[0].inheritable &= ~((1u << CAP_DAC_OVERRIDE) + | (1u << CAP_DAC_READ_SEARCH)); + TEST_COMPARE (syscall (SYS_capset, &header, data), 0); + + /* capset leaves the bounding set untouched, and a namespace-root + execve re-grants permitted = bounding | inheritable (effective + too), so the exec'd loader would regain CAP_DAC_OVERRIDE and could + open the execute-only file by path. Drop the caps from the + bounding set as well so file permissions bind across the exec. */ + TEST_COMPARE (prctl (PR_CAPBSET_DROP, CAP_DAC_OVERRIDE, 0, 0, 0), 0); + TEST_COMPARE (prctl (PR_CAPBSET_DROP, CAP_DAC_READ_SEARCH, 0, 0, 0), 0); +} + +static struct support_capture_subprocess +run_prog_copy (const char *arg1, const char *arg2, bool trace) +{ + char *argv[] = { prog_copy, (char *) arg1, (char *) arg2, NULL }; + char *envp[3] = { libpath_env, NULL, NULL }; + if (trace) + envp[1] = (char *) "LD_TRACE_LOADED_OBJECTS=1"; + return support_capture_subprogram (prog_copy, argv, envp); +} + +static int +do_test (void) +{ + /* The namespace's uid/gid 0 must be mapped (support_become_root + identity-maps the original ids instead): the binfmt_misc inodes + are owned by the namespace's uid 0, and an unmapped owner cannot + pass the permission (or capability) checks for writing to the + register file. */ + if (!support_become_ns_root ()) + FAIL_UNSUPPORTED ("cannot create user+mount namespace with uid 0"); + + /* A binfmt_misc instance mounted in a user namespace is private to + it: registrations neither affect nor require anything from the + host (Linux >= 6.7). */ + binfmt_dir = support_create_temp_directory ("tst-rtld-execfd-binfmt-"); + if (mount ("binfmt_misc", binfmt_dir, "binfmt_misc", 0, NULL) != 0) + { + if (errno == ENODEV || errno == ENOENT || errno == ENOSYS + || errno == EPERM || errno == EACCES || errno == EINVAL) + FAIL_UNSUPPORTED ("cannot mount binfmt_misc: %m"); + FAIL_EXIT1 ("mount binfmt_misc: %m"); + } + atexit (unmount_binfmt); + + /* Register the dynamic linker under test as an extension handler + with the 'O' flag, so the kernel keeps the executed binary open + and passes it in AT_EXECFD. */ + { + char *reg = xasprintf (":tst-rtld-execfd:E::" EXT "::%s:O", + support_objdir_elf_ldso); + char *regpath = xasprintf ("%s/register", binfmt_dir); + support_write_file_string (regpath, reg); + free (regpath); + free (reg); + } + + /* The handler execs ld.so without options, so the helper must find + the build-tree libraries through the environment. */ + libpath_env = xasprintf ("LD_LIBRARY_PATH=%s:%s/elf", + support_objdir_root, support_objdir_root); + + char *prog = xasprintf ("%s/elf/tst-rtld-execfd-prog", + support_objdir_root); + char *tmpdir = support_create_temp_directory ("tst-rtld-execfd-"); + prog_copy = xasprintf ("%s/prog." EXT, tmpdir); + support_copy_file (prog, prog_copy); + add_temp_file (prog_copy); + free (tmpdir); + free (prog); + + /* Execute the helper through the handler. The application must + observe exactly what a direct execution would produce: the argv + the kernel spliced for the interpreter is consumed by ld.so as + usual, and the descriptor is loaded from and neutralized. */ + { + struct support_capture_subprocess cap + = run_prog_copy ("first-arg", "second-arg", false); + support_capture_subprocess_check (&cap, "execfd", 0, sc_allow_stdout); + char *expected = xasprintf ("argc=3\n" + "argv[0]=%s\n" + "argv[1]=first-arg\n" + "argv[2]=second-arg\n" + "AT_EXECFD=0 errno=%d\n" + "AT_EXECFN=%s\n", + prog_copy, ENOENT, prog_copy); + TEST_COMPARE_STRING (cap.out.buffer, expected); + free (expected); + support_capture_subprocess_free (&cap); + } + + /* LD_TRACE_LOADED_OBJECTS (ldd) must work for a descriptor-loaded + main program. */ + { + struct support_capture_subprocess cap + = run_prog_copy (NULL, NULL, true); + support_capture_subprocess_check (&cap, "execfd trace", 0, + sc_allow_stdout); + TEST_VERIFY (strstr (cap.out.buffer, "libc.so") != NULL); + support_capture_subprocess_free (&cap); + } + + /* A program whose spliced path begins with "--" must be run, not + mistaken for a dynamic-linker option: the kernel copies the + execve() pathname into the interpreter's argument vector verbatim, + so a program named like a loader option would otherwise divert or + abort the loader. A relative path is required for the kernel to + hand ld.so an argument starting with "--"; an absolute path always + begins with "/". */ + { + const char *dashrel = "--library-path." EXT; + char *dashdir = support_create_temp_directory ("tst-rtld-execfd-dash-"); + char *dashabs = xasprintf ("%s/%s", dashdir, dashrel); + support_copy_file (prog_copy, dashabs); + add_temp_file (dashabs); + + xchdir (dashdir); + char *argv[] = { (char *) dashrel, (char *) "tail", NULL }; + char *envp[2] = { libpath_env, NULL }; + struct support_capture_subprocess cap + = support_capture_subprogram (dashrel, argv, envp); + xchdir ("/"); + support_capture_subprocess_check (&cap, "execfd dashname", 0, + sc_allow_stdout); + char *expected = xasprintf ("argc=2\n" + "argv[0]=%s\n" + "argv[1]=tail\n" + "AT_EXECFD=0 errno=%d\n" + "AT_EXECFN=%s\n", + dashrel, ENOENT, dashrel); + TEST_COMPARE_STRING (cap.out.buffer, expected); + free (expected); + support_capture_subprocess_free (&cap); + free (dashabs); + free (dashdir); + } + + /* The headline capability: an execute-only binary cannot be opened + by path, but execve() permits it and the descriptor the kernel + passes is readable. Give up the DAC-override capabilities first, + otherwise this (namespace-root) process could open it anyway. + This must be the last subtest: the capabilities are gone. */ + { + TEST_COMPARE (chmod (prog_copy, 0111), 0); + drop_dac_capabilities (); + + /* Control: the path really is unopenable now. This is what any + path-based re-open in ld.so would run into. */ + errno = 0; + TEST_COMPARE (open (prog_copy, O_RDONLY), -1); + TEST_COMPARE (errno, EACCES); + + struct support_capture_subprocess cap + = run_prog_copy ("x-only", NULL, false); + support_capture_subprocess_check (&cap, "execfd execute-only", 0, + sc_allow_stdout); + TEST_VERIFY (strstr (cap.out.buffer, "argv[1]=x-only") != NULL); + TEST_VERIFY (strstr (cap.out.buffer, "AT_EXECFD=0 errno=2") != NULL); + support_capture_subprocess_free (&cap); + } + + return 0; +} + +#include From patchwork Fri Jul 17 11:12:00 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Christian Brauner X-Patchwork-Id: 139401 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 550774BA2E0A for ; Fri, 17 Jul 2026 11:13:03 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 550774BA2E0A Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=DS6Pcss0 X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from sea.source.kernel.org (sea.source.kernel.org [172.234.252.31]) by sourceware.org (Postfix) with ESMTPS id 1B3D44BA2E10 for ; Fri, 17 Jul 2026 11:12:15 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 1B3D44BA2E10 Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=kernel.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=kernel.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 1B3D44BA2E10 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=172.234.252.31 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286735; cv=none; b=xEGWV7AbvnYSL90jdrgCCTRVamr4UeL9MWopVFEfPD+wS6r944hOwdhKyb2SdGAGBGub47OSn3bV+Iz4HHD21o+Ygx5UFRfZjEc166WsNzABVMZHe1KseqFaX3P7VCKyhMo+8K/N+2Rw/2SrSgvhSf9+m8nJygfDQqAq/Imlgb8= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286735; c=relaxed/simple; bh=5plWmBnmxNGHHBG2tT8019oFCCSb61O1Kn1zf986/xk=; h=DKIM-Signature:From:Date:Subject:MIME-Version:Message-Id:To; b=Fgr6MOJ+Jg33mJw4ePdh0+o18Xec2n/PWo4gHLVyCYui36P0qMoo30KZR83v3rbJqVtXpKgvfWfC7ter6fXmGe1HwV4e+5RBA0/nmklUmrTKZUFM5bRRujt/4sRbRuIVqBjjnUrvJkrm91yr+25iy02597RBrvnvbAaVq2WoM8A= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=DS6Pcss0 DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 1B3D44BA2E10 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) by sea.source.kernel.org (Postfix) with ESMTP id 75DAC43704; Fri, 17 Jul 2026 11:12:14 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id E2B851F000E9; Fri, 17 Jul 2026 11:12:12 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784286734; bh=PS26PKab5KICOrq36Fz42Nm8Ugifo6FrEl9LNmH+00E=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=DS6Pcss0jWfC4MBd2kSs189h++9o6tf9DmmZ6oSaCilsbsCB186bM9SJbONp/eA6L RvK7MetMVQQXKzH0iPTF0OviLHoQmpqvqEWn0CiOrhLJo59tHx3HBwia8fO2PsKJaz yOgr7khuXy8ZHh2agNOYUMfpsHJngpu7Nz5nycdTun8Axkig8Rzho53eXpbv/65HvU KEyxcRwbAW1ZMwiXJ/yH5xnf9fCc5xThUEFy2dxYCgwdOyz16S7FDmchIarAORG9Xc x/je0KyGsV+vvhjxzsf9ErVsIM1qpJooOnS0kfrys2C8yOksXg58ApCeq0BET4Hf0o nh1sMRaBMM71g== From: Christian Brauner Date: Fri, 17 Jul 2026 13:12:00 +0200 Subject: [PATCH v3 3/4] elf: add ld.so --program-fd MIME-Version: 1.0 Message-Id: <20260717-work-glibc-binfmt_misc-v3-3-45129bfb13fe@kernel.org> References: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> In-Reply-To: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> To: Florian Weimer , libc-alpha@sourceware.org Cc: Adhemerval Zanella Netto , Carlos O'Donell , "Christian Brauner (Amutable)" X-Mailer: b4 0.16-dev-4217c X-Developer-Signature: v=1; a=openpgp-sha256; l=6869; i=brauner@kernel.org; h=from:subject:message-id; bh=5plWmBnmxNGHHBG2tT8019oFCCSb61O1Kn1zf986/xk=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRF8bHemy0+Z/2r/MOrdc95XroaYBbxYtGKwrjABR9qZ fKexYdf7yhlYRDjYpAVU2RxaDcJl1vOU7HZKFMDZg4rE8gQBi5OAZjIrm5GhkUr9X9+752hJllm u2TJlvz0Ca9D6tu4J+/Uj+mp8C7lj2f4K7XdImD/zISP1jnxse/YqpLD3PRXTpOq2BDT5TCjaJ8 oKwA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 X-Spam-Status: No, score=-10.2 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, SPF_HELO_NONE, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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 descriptor without a usable path such as a sealed memfd, $ORIGIN degrades to "/", just as it does for fexecve; a comment notes this. The descriptor number is parsed with _dl_strtoul like the loader's other numbers, but rejected unless it is a bare non-negative decimal in range, so a signed or zero-padded argument cannot select an unintended descriptor. 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 secure standard-descriptor recheck added with AT_EXECFD already covers a descriptor from either source. Signed-off-by: Christian Brauner (Amutable) --- NEWS | 6 ++++++ elf/dl-load.c | 4 +++- elf/dl-usage.c | 2 ++ elf/rtld.c | 35 +++++++++++++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 3 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/dl-load.c b/elf/dl-load.c index f2f14dd6f3..6cc9a22010 100644 --- a/elf/dl-load.c +++ b/elf/dl-load.c @@ -2249,7 +2249,9 @@ _dl_map_object (struct link_map *loader, const char *name, that kept the executed binary open) or from an explicit loader invocation. NAME is the name the program is known by and is only used for diagnostics; the canonical name used for $ORIGIN is derived - from the descriptor itself (__RTLD_OPENEXEC). There may be no path + from the descriptor itself (__RTLD_OPENEXEC). For a descriptor + without a usable path - a sealed memfd, say - the origin degrades + to "/", just as it does for fexecve. There may be no path the program could be opened by: the descriptor is readable even for an execute-only binary, and it refers to the very file the kernel access-checked, so no path re-open takes its place. */ 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 3d383ae3b9..778bfd3856 100644 --- a/elf/rtld.c +++ b/elf/rtld.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -603,6 +604,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; }; @@ -640,8 +644,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 @@ -792,6 +799,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; @@ -1514,6 +1522,28 @@ 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 as + FreeBSD's ld-elf.so.1 -f). Parse the descriptor with + _dl_strtoul as the loader parses its other numbers, then + reject what it would accept for a plain descriptor - a sign, + leading whitespace, a base prefix - and require a bare decimal + ("0" or [1-9][0-9]*), fully consumed and in range, like pldd's + pid check. */ + const char *arg = _dl_argv[2]; + char *endp; + uint64_t fd = _dl_strtoul (arg, &endp); + if (arg[0] < '0' || arg[0] > '9' || *endp != '\0' + || (arg[0] == '0' && arg[1] != '\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; } @@ -1633,6 +1663,7 @@ dl_main (const ElfW(Phdr) *phdr, args.str = rtld_progname; args.loader = NULL; args.mode = __RTLD_OPENEXEC; + args.execfd = execfd; (void) _dl_catch_error (&objname, &err_str, &malloced, map_doit, &args); if (__glibc_unlikely (err_str != NULL)) From patchwork Fri Jul 17 11:12:01 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Christian Brauner X-Patchwork-Id: 139403 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id E7EB14BA5435 for ; Fri, 17 Jul 2026 11:14:12 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org E7EB14BA5435 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=WoxXN1yl X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from sea.source.kernel.org (sea.source.kernel.org [IPv6:2600:3c0a:e001:78e:0:1991:8:25]) by sourceware.org (Postfix) with ESMTPS id 6BE174BA2E0D for ; Fri, 17 Jul 2026 11:12:17 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 6BE174BA2E0D Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=kernel.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=kernel.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 6BE174BA2E0D Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2600:3c0a:e001:78e:0:1991:8:25 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286737; cv=none; b=ew15AG1l52l71SiR0yFsEN7+Wfg8Ehrbdrv3BO4XXsDUIJ6RL2QkvnCVazWdUDBGz6R+SbUPvblOADVkEM1ZG43vxNLrQLsNQz6Zz1F/3G4zrV82khuIDgr0kDNBrR1QHI67dUdifA6IS3HDj0ue//PrLvkwoEHt2r6R01sVwQM= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1784286737; c=relaxed/simple; bh=cQvsRSRIr9i46bds03I2TcAGZLBCadz10YlXp7ox6ko=; h=DKIM-Signature:From:Date:Subject:MIME-Version:Message-Id:To; b=cr80rxsEgCXdcN1NwopqgdT12QQ/7qC89mHDbowXsPYBltUS4sc7ANDdm/pnNXqVM+/9qsb60iP/ZJpmTWkxFaxQvkYu7rJPILXpIzhKx0f0KLqGmB4YCnd21Zb5jGG5YqeiyBDUTPNnweEUoGX3aJqIpH21B07o64kSLAW/eiU= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=WoxXN1yl DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 6BE174BA2E0D Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) by sea.source.kernel.org (Postfix) with ESMTP id C20C44077B; Fri, 17 Jul 2026 11:12:16 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id EBEF11F000E9; Fri, 17 Jul 2026 11:12:14 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784286736; bh=voefQkomkXzsTAlvC7A9zFDm90ILT1z4EqKes8/75Xw=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=WoxXN1yl3oA3leBWzY0V354Zh3IlkgLJRu5X59Zo+UXQvsH8PKwaf1ArED3F9llfZ K/LKiMCrYpq0FWcqystv0+a1E22AQ9xwd02QxA4vZCLxJJc3ytxxHYRW7J5BWBgMIa oDWs7KTXvWaZlzfH6TK8B+6xe2Q3aNkE6Q79iHai8Hy4F2kD/MjatX8tVS4FdEwkXZ 8h7SAGLTNBNG8DYEhejF9bj/LG0IXOb3DHUOri202xnPR6rvS0aIdA7Clx3kVwjO8q H5HoXvcyNz75E82lcLp3i9VnU2V4cFx7RRoxLUT+E9aiOj/C5Dfam1PmBWS9yTmm3F GS9kkDN9+amHA== From: Christian Brauner Date: Fri, 17 Jul 2026 13:12:01 +0200 Subject: [PATCH v3 4/4] elf: test ld.so --program-fd MIME-Version: 1.0 Message-Id: <20260717-work-glibc-binfmt_misc-v3-4-45129bfb13fe@kernel.org> References: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> In-Reply-To: <20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org> To: Florian Weimer , libc-alpha@sourceware.org Cc: Adhemerval Zanella Netto , Carlos O'Donell , "Christian Brauner (Amutable)" X-Mailer: b4 0.16-dev-4217c X-Developer-Signature: v=1; a=openpgp-sha256; l=8955; i=brauner@kernel.org; h=from:subject:message-id; bh=cQvsRSRIr9i46bds03I2TcAGZLBCadz10YlXp7ox6ko=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRF8bFpvrMRYpG9mXKl/sn3KP6jLVJcNu1TGX/a6SXN6 Gc/Y/O1o5SFQYyLQVZMkcWh3SRcbjlPxWajTA2YOaxMIEMYuDgFYCLTljEyHL/97PvMSuOfX2ae f36HUeD+/M7O4Cs2BZ/LNxcw5VUpZzMyzPl0sV3g1jvtNUXc1g8MtHZX6x23WZwdE6MXkv9lnls sOwA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 X-Spam-Status: No, score=-10.3 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, SPF_HELO_NONE, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org Run the dynamic linker with --program-fd on an inherited descriptor of a helper program: it must be loaded from the descriptor, with the program name argument only naming it. This exercises the same loading path as AT_EXECFD without any kernel support, giving the descriptor-loading code deterministic coverage on kernels and CI setups where the binfmt_misc test reports UNSUPPORTED. Unlike a descriptor installed by the kernel, one given on the command line need not be positioned at the start of the file, so one subtest hands the loader a descriptor deliberately seeked mid-file and verifies that the program runs and that the shared file position is left undisturbed (the ELF header is read with pread). The remaining subtests verify that --program-fd composes with --argv0 and that a closed descriptor or a non-numeric argument produces a clean error, not a crash. Signed-off-by: Christian Brauner (Amutable) --- elf/Makefile | 4 ++ elf/tst-rtld-program-fd-prog.c | 28 ++++++++ elf/tst-rtld-program-fd.c | 150 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) 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/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 . */ + +#include + +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..9c3966943f --- /dev/null +++ b/elf/tst-rtld-program-fd.c @@ -0,0 +1,150 @@ +/* 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 . */ + +/* 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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); + } + + /* The descriptor's file position must be irrelevant and preserved: + the loader reads the ELF header with pread. Hand over the + descriptor deliberately positioned mid-file and verify that the + program still runs and that the position - shared with the + subprocess - is where it was left. */ + { + xlseek (fd, 123, SEEK_SET); + char *argv[] = + { + (char *) "ld.so", (char *) "--library-path", libpath, + (char *) "--program-fd", fdstr, + (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 position", 0, + sc_allow_stdout); + TEST_COMPARE_STRING (cap.out.buffer, + "argc=1\n" + "argv[0]=displayed-name\n"); + TEST_COMPARE (xlseek (fd, 0, SEEK_CUR), 123); + support_capture_subprocess_free (&cap); + } + + /* Composes with --argv0. No rewind: the previous subtest left the + position mid-file, which the loader ignores. */ + { + 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