BPF List
 help / color / mirror / Atom feed
* [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution
@ 2026-07-20  9:33 Christian Brauner
  2026-07-20  9:33 ` [PATCH 01/21] exec: do not act on a stale execfd request without an executable Christian Brauner
                   ` (20 more replies)
  0 siblings, 21 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable), stable

binfmt_misc has exactly one execution model where the registered
interpreter becomes the executed program and the matched binary is
handed to it as an argument. For wine or qemu-user that is the point.
For a per-binary loader it is backwards. The interpreter is an
implementation detail of running the binary, yet it owns the entire
process identity:

- argv[0] and /proc/pid/cmdline show the interpreter invocation, not
  what the caller executed.

- /proc/self/exe names the interpreter. Relocatable programs commonly
  locate themselves through it and find the dynamic linker instead.

- A binary passed to execveat() as an inaccessible O_CLOEXEC fd
  cannot run at all as the interpreter has no path to open it by.

- gdb cross-validates AT_ENTRY/AT_PHDR against the exe file and
  discards the load displacement on mismatch leaving PIE symbols
  unrelocated.

This series adds two dispatch modes that close the gap from opposite
ends:

(1) transparent dispatch

   Registered with the 'T' flag or chosen per exec with
   BPF_BINPRM_TRANSPARENT. The binary travels through AT_EXECFD, the
   argument vector stays exactly as the caller built it, and the kernel
   labels mm->exe_file and comm with the binary. A new aux vector bit,
   AT_FLAGS_TRANSPARENT_INTERP, is raised indicating that nothing
   was spliced, argv belongs to the program, and to load it from the
   descriptor.

   The interpreter keeps control of mapping the binary, so the mode
   covers foreign architectures and non-ELF payloads.

   The exe label is not a new privilege. It names precisely the file the
   caller passed to execve(), not a file of the process's choosing. That
   file is permission-checked, write-denied while the process runs and
   recorded by audit. Credential derivation does not change exactly as
   today.

(2) The second is loader substitution. The kernel executes the matched
    binary natively as the main image and substitutes the registered
    interpreter for the loader named in the binary's PT_INTERP.
    binfmt_misc stops meaning becomes a PT_INTERP override. There is no
    contract and no identity to reconstruct, so a stock dynamic loader
    works unchanged. So 'L' is for native-arch ELF with PT_INTERP. The
    modes compose. A bpf handler reads the ELF header from bprm->buf and
    grades per binary, picking 'L' where it applies and 'T' or classic
    dispatch for the rest.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Christian Brauner (21):
      exec: do not act on a stale execfd request without an executable
      docs, binfmt_misc: keep general usage out of the handler sections
      binfmt_misc: table-drive the register string flags
      binfmt_misc: normalize the per-exec invocation flags
      binfmt_misc: split out entry_open_interpreter()
      binfmt_misc: split out build_interp_argv()
      exec: release the replaced file with do_close_execat()
      exec: add AT_FLAGS_TRANSPARENT_INTERP
      exec: label mm->exe_file with the binary for a transparent dispatch
      binfmt_misc: add transparent interpreter dispatch
      binfmt_misc: add a static transparent flag 'T'
      binfmt_misc: let a bpf handler run the interpreter transparently
      selftests/exec: convert the binfmt_misc bpf test to the kselftest harness
      selftests/exec: test the transparent binfmt_misc mode
      binfmt_misc: document the transparent identity contract
      exec: carry a PT_INTERP substitute in struct linux_binprm
      binfmt_elf: consume a stashed PT_INTERP substitute
      binfmt_misc: add the 'L' loader substitution flag
      binfmt_misc: let a bpf handler request loader substitution
      selftests/exec: test binfmt_misc loader substitution
      binfmt_misc: document loader substitution

 Documentation/admin-guide/binfmt-misc.rst          | 159 +++++++--
 fs/binfmt_elf.c                                    |  20 +-
 fs/binfmt_elf_fdpic.c                              |   5 +-
 fs/binfmt_misc.c                                   | 311 ++++++++++++------
 fs/binfmt_misc_bpf.c                               |  18 +-
 fs/exec.c                                          |  54 ++-
 include/linux/binfmt_misc.h                        |   8 +
 include/linux/binfmts.h                            |   9 +-
 include/uapi/linux/binfmts.h                       |   7 +
 tools/testing/selftests/exec/.gitignore            |   5 +
 tools/testing/selftests/exec/Makefile              |  30 +-
 .../testing/selftests/exec/binfmt_loader_payload.c | 196 +++++++++++
 tools/testing/selftests/exec/binfmt_misc_bpf.c     | 362 +++++++++++---------
 tools/testing/selftests/exec/binfmt_misc_common.h  | 146 +++++++++
 tools/testing/selftests/exec/binfmt_misc_loader.c  | 363 +++++++++++++++++++++
 .../selftests/exec/binfmt_misc_transparent.c       | 108 ++++++
 .../selftests/exec/binfmt_transparent_interp.c     | 130 ++++++++
 tools/testing/selftests/exec/loader.bpf.c          |  56 ++++
 tools/testing/selftests/exec/transparent.bpf.c     |  57 ++++
 19 files changed, 1748 insertions(+), 296 deletions(-)
---
base-commit: 34296289b1a63a5394271b4e35918d3c9ad530dc
change-id: 20260719-work-bpf-binfmt_misc-ptinterp-e6ee99ff6ba8


^ permalink raw reply	[flat|nested] 28+ messages in thread

* [PATCH 01/21] exec: do not act on a stale execfd request without an executable
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20 10:00   ` sashiko-bot
  2026-07-20  9:33 ` [PATCH 02/21] docs, binfmt_misc: keep general usage out of the handler sections Christian Brauner
                   ` (19 subsequent siblings)
  20 siblings, 1 reply; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable), stable

binfmt_misc sets bprm->have_execfd (and, for a 'C' entry, execfd_creds)
before it opens the interpreter. When that open fails the entry returns
-ENOEXEC, but the two flags stay set on the bprm. As binfmt_misc is
inserted at the head of the format list, the search then continues to a
later format which never staged an interpreter, so bprm->executable is
NULL while the flags falsely claim an execfd is present.

begin_new_exec() then dereferences the missing executable:

  - bprm_creds_from_file() computes credentials from
    execfd_creds ? bprm->executable : bprm->file and faults on the NULL;
  - would_dump(bprm, bprm->executable) faults the same way; and
  - create_elf_tables() would emit AT_EXECFD from the zero-initialised
    bprm->execfd, handing the program descriptor 0.

An unprivileged user can reach this. Mount binfmt_misc in a user
namespace, register an 'O' or 'C' entry whose interpreter lives on a
FUSE mount, have the FUSE server fail the open with -ENOEXEC (which
fuse_dev_do_write() relays verbatim), and execute a native ELF file
matching the entry. The result is a NULL pointer dereference in
begin_new_exec().

have_execfd and execfd_creds only mean anything alongside the
bprm->executable they describe. Clear them when no executable was
staged, before the first dereference, so the exec proceeds as the plain
execution of the binary it has become.

Fixes: bc2bf338d54b ("exec: Remove recursion from search_binary_handler")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/exec.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/fs/exec.c b/fs/exec.c
index c698aabe9abd..2f50aa039827 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1112,6 +1112,12 @@ int begin_new_exec(struct linux_binprm * bprm)
 	struct task_struct *me = current;
 	int retval;
 
+	/* A declined execfd request has no executable for a later format. */
+	if (!bprm->executable) {
+		bprm->have_execfd = 0;
+		bprm->execfd_creds = 0;
+	}
+
 	/* Once we are committed compute the creds */
 	retval = bprm_creds_from_file(bprm);
 	if (retval)

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 02/21] docs, binfmt_misc: keep general usage out of the handler sections
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
  2026-07-20  9:33 ` [PATCH 01/21] exec: do not act on a stale execfd request without an executable Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 03/21] binfmt_misc: table-drive the register string flags Christian Brauner
                   ` (18 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

The general usage trails the bpf-backed handlers section and therefore
reads as part of it. It predates that section and applies to binfmt_misc
as a whole.

Move it back up so the handler section ends where the file does.
Upcoming sections describing the transparent and loader dispatch modes
append after it without swallowing the general prose again.

Pure text move, no content changes.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst | 79 ++++++++++++++++---------------
 1 file changed, 40 insertions(+), 39 deletions(-)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 7f42abf9cfba..c76381a4bcad 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -100,6 +100,46 @@ There are some restrictions:
  - the interpreter string may not exceed 127 characters
 
 
+To use binfmt_misc you have to mount it first. You can mount it with
+``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add
+a line ``none  /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your
+``/etc/fstab`` so it auto mounts on boot.
+
+You may want to add the binary formats in one of your ``/etc/rc`` scripts during
+boot-up. Read the manual of your init program to figure out how to do this
+right.
+
+Think about the order of adding entries! Later added entries are matched first!
+
+
+A few examples (assumed you are in ``/proc/sys/fs/binfmt_misc``):
+
+- enable support for em86 (like binfmt_em86, for Alpha AXP only)::
+
+    echo ':i386:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register
+    echo ':i486:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register
+
+- enable support for packed DOS applications (pre-configured dosemu hdimages)::
+
+    echo ':DEXE:M::\x0eDEX::/usr/bin/dosexec:' > register
+
+- enable support for Windows executables using wine::
+
+    echo ':DOSWin:M::MZ::/usr/local/bin/wine:' > register
+
+For java support see Documentation/admin-guide/java.rst
+
+
+You can enable/disable binfmt_misc or one binary type by echoing 0 (to disable)
+or 1 (to enable) to ``/proc/sys/fs/binfmt_misc/status`` or
+``/proc/.../the_name``.
+Catting the file tells you the current status of ``binfmt_misc/the_entry``.
+
+You can remove one entry or all entries by echoing -1 to ``/proc/.../the_name``
+or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed
+by simply unlinking (``rm``) ``/proc/.../the_name``.
+
+
 bpf-backed handlers
 -------------------
 
@@ -157,45 +197,6 @@ registered in, falling back to ancestor namespaces, mirroring how
 binfmt_misc instances themselves are looked up. The entry keeps the handler
 alive; deleting the struct_ops map only prevents new activations.
 
-To use binfmt_misc you have to mount it first. You can mount it with
-``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add
-a line ``none  /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your
-``/etc/fstab`` so it auto mounts on boot.
-
-You may want to add the binary formats in one of your ``/etc/rc`` scripts during
-boot-up. Read the manual of your init program to figure out how to do this
-right.
-
-Think about the order of adding entries! Later added entries are matched first!
-
-
-A few examples (assumed you are in ``/proc/sys/fs/binfmt_misc``):
-
-- enable support for em86 (like binfmt_em86, for Alpha AXP only)::
-
-    echo ':i386:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register
-    echo ':i486:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register
-
-- enable support for packed DOS applications (pre-configured dosemu hdimages)::
-
-    echo ':DEXE:M::\x0eDEX::/usr/bin/dosexec:' > register
-
-- enable support for Windows executables using wine::
-
-    echo ':DOSWin:M::MZ::/usr/local/bin/wine:' > register
-
-For java support see Documentation/admin-guide/java.rst
-
-
-You can enable/disable binfmt_misc or one binary type by echoing 0 (to disable)
-or 1 (to enable) to ``/proc/sys/fs/binfmt_misc/status`` or
-``/proc/.../the_name``.
-Catting the file tells you the current status of ``binfmt_misc/the_entry``.
-
-You can remove one entry or all entries by echoing -1 to ``/proc/.../the_name``
-or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed
-by simply unlinking (``rm``) ``/proc/.../the_name``.
-
 
 Hints
 -----

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 03/21] binfmt_misc: table-drive the register string flags
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
  2026-07-20  9:33 ` [PATCH 01/21] exec: do not act on a stale execfd request without an executable Christian Brauner
  2026-07-20  9:33 ` [PATCH 02/21] docs, binfmt_misc: keep general usage out of the handler sections Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 04/21] binfmt_misc: normalize the per-exec invocation flags Christian Brauner
                   ` (17 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Every flag character of the register string is spelled out three
times: in the parser, in the entry's /proc output and in the
delimiter blacklist that keeps a flag character from sending the flag
scan off the end of the buffer. The three lists have to agree, and
each new flag has to be added to all of them.

Describe a flag once - character, entry flag, implied flags and a
description for the registration debug output - and drive all three
from the table.

While at it, express the "a 'B' entry carries no flags" check as what
it is, an empty flags field, rather than as a fourth list of every
flag character. Equivalent: the check runs right after
check_special_flags(), which advances past exactly the flag
characters it consumed and sets exactly their flags.

No functional change.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_misc.c | 92 ++++++++++++++++++++++++++++++++------------------------
 1 file changed, 52 insertions(+), 40 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index a3175521c8dd..521ddda929a4 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -10,6 +10,7 @@
 
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
+#include <linux/array_size.h>
 #include <linux/binfmt_misc.h>
 #include <linux/binfmts.h>
 #include <linux/bitops.h>
@@ -51,6 +52,36 @@ enum binfmt_misc_entry_flags {
 	MISC_FMT_OPEN_FILE	= (1U << 28),
 };
 
+/**
+ * struct binfmt_misc_flag - a flag character of the register string
+ * @c: the character userspace writes and reads back
+ * @flag: the entry flag it sets
+ * @implies: entry flags it turns on in addition
+ * @desc: what it does, for the registration debug output
+ */
+struct binfmt_misc_flag {
+	char		c;
+	unsigned long	flag;
+	unsigned long	implies;
+	const char	*desc;
+};
+
+static const struct binfmt_misc_flag misc_flags[] = {
+	{ 'P', MISC_FMT_PRESERVE_ARGV0,	0,			"preserve argv0"		},
+	{ 'O', MISC_FMT_OPEN_BINARY,	0,			"open binary"			},
+	{ 'C', MISC_FMT_CREDENTIALS,	MISC_FMT_OPEN_BINARY,	"credentials from the binary"	},
+	{ 'F', MISC_FMT_OPEN_FILE,	0,			"open interpreter file now"	},
+};
+
+/* Look up a flag character, NULL if @c is not one. */
+static const struct binfmt_misc_flag *misc_flag_by_char(const char c)
+{
+	for (int i = 0; i < ARRAY_SIZE(misc_flags); i++)
+		if (misc_flags[i].c == c)
+			return &misc_flags[i];
+	return NULL;
+}
+
 struct binfmt_misc_entry {
 	struct hlist_node node;
 	unsigned long flags;		/* type, status, etc. */
@@ -423,30 +454,16 @@ static char *scanarg(char *s, char del)
 	return s;
 }
 
+/* Parse the 'flags' field, stopping at the first character that is not one. */
 static char *check_special_flags(char *p, struct binfmt_misc_entry *e)
 {
 	for (;; p++) {
-		switch (*p) {
-		case 'P':
-			pr_debug("register: flag: P (preserve argv0)\n");
-			e->flags |= MISC_FMT_PRESERVE_ARGV0;
-			break;
-		case 'O':
-			pr_debug("register: flag: O (open binary)\n");
-			e->flags |= MISC_FMT_OPEN_BINARY;
-			break;
-		case 'C':
-			pr_debug("register: flag: C (preserve creds)\n");
-			/* C implies O */
-			e->flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY;
-			break;
-		case 'F':
-			pr_debug("register: flag: F: open interpreter file now\n");
-			e->flags |= MISC_FMT_OPEN_FILE;
-			break;
-		default:
+		const struct binfmt_misc_flag *f = misc_flag_by_char(*p);
+
+		if (!f)
 			return p;
-		}
+		pr_debug("register: flag: %c (%s)\n", f->c, f->desc);
+		e->flags |= f->flag | f->implies;
 	}
 }
 
@@ -569,7 +586,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 					      size_t count)
 {
 	struct binfmt_misc_entry *e __free(kfree) = NULL;
-	char *buf, *p;
+	char *buf, *p, *flags;
 	char del;
 
 	pr_debug("register: received %zu bytes\n", count);
@@ -594,7 +611,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 	pr_debug("register: delim: %#x {%c}\n", del, del);
 
 	/* A flag-char delimiter runs the flag scan off the buffer. */
-	if (del == 'P' || del == 'O' || del == 'C' || del == 'F')
+	if (misc_flag_by_char(del))
 		return ERR_PTR(-EINVAL);
 
 	/* Pad the buffer with the delim to simplify parsing below. */
@@ -665,21 +682,21 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 	}
 
 	/* Parse the 'flags' field. */
+	flags = p;
 	p = check_special_flags(p, e);
-	if (*p == '\n')
-		p++;
-	if (p != buf + count)
-		return ERR_PTR(-EINVAL);
 
 	/*
 	 * A bpf handler decides the invocation flags per exec with
-	 * bpf_binprm_set_flags() rather than fixing them at registration, so a
-	 * 'B' entry carries no flags: 'P', 'C' and 'O' become per-exec choices
-	 * and 'F' (pre-open a fixed interpreter) is meaningless for it.
+	 * bpf_binprm_set_flags() rather than fixing them at registration, and
+	 * 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B'
+	 * entry's flags field has to be empty.
 	 */
-	if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
-	    (e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY |
-			 MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags)
+		return ERR_PTR(-EINVAL);
+
+	if (*p == '\n')
+		p++;
+	if (p != buf + count)
 		return ERR_PTR(-EINVAL);
 
 	return no_free_ptr(e);
@@ -742,14 +759,9 @@ static int bm_entry_show(struct seq_file *m, void *unused)
 
 	/* print the special flags */
 	seq_puts(m, "flags: ");
-	if (e->flags & MISC_FMT_PRESERVE_ARGV0)
-		seq_putc(m, 'P');
-	if (e->flags & MISC_FMT_OPEN_BINARY)
-		seq_putc(m, 'O');
-	if (e->flags & MISC_FMT_CREDENTIALS)
-		seq_putc(m, 'C');
-	if (e->flags & MISC_FMT_OPEN_FILE)
-		seq_putc(m, 'F');
+	for (int i = 0; i < ARRAY_SIZE(misc_flags); i++)
+		if (e->flags & misc_flags[i].flag)
+			seq_putc(m, misc_flags[i].c);
 	seq_putc(m, '\n');
 
 	if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 04/21] binfmt_misc: normalize the per-exec invocation flags
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (2 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 03/21] binfmt_misc: table-drive the register string flags Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 05/21] binfmt_misc: split out entry_open_interpreter() Christian Brauner
                   ` (16 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

A static entry fixes its invocation flags at registration. A 'B' entry's
load program picks them per exec. Since load_misc_binary() branches on
which kind of entry matched and then applies the two flag sets side by
side every flag is handled twice and each new one has to be added to
both arms.

Translate the 'B' flags into the entry flags they mirror and let the
dispatch act on a single set of flags. The boolean the two arms
communicated 'P' can be removed.

No functional change.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_misc.c | 66 +++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 41 insertions(+), 25 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 521ddda929a4..7a6bbeb8a58c 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -318,6 +318,40 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
 	return ERR_PTR(retval);
 }
 
+/**
+ * entry_invocation_flags - the invocation flags in effect for this exec
+ * @e: matched binary type handler
+ * @bprm: binary that is being executed
+ *
+ * A static entry fixes its flags at registration, a 'B' entry's load program
+ * picks them per exec with bpf_binprm_set_flags(). Translate the latter into
+ * the former, implications included, so the dispatch has one set to act on.
+ *
+ * Return: the invocation flags for this exec
+ */
+static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
+					    struct linux_binprm *bprm)
+{
+	unsigned long flags = 0;
+	u64 bpf_flags;
+
+	if (!test_bit(MISC_FMT_BPF_BIT, &e->flags))
+		return e->flags;
+
+	bpf_flags = bprm->bpf_flags;
+	/* Clear so they can't accumulate into a nested interpreter level. */
+	bprm->bpf_flags = 0;
+
+	if (bpf_flags & BPF_BINPRM_PRESERVE_ARGV0)
+		flags |= MISC_FMT_PRESERVE_ARGV0;
+	if (bpf_flags & BPF_BINPRM_EXECFD)
+		flags |= MISC_FMT_OPEN_BINARY;
+	if (bpf_flags & BPF_BINPRM_CREDENTIALS)
+		flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY;
+
+	return flags;
+}
+
 /*
  * the loader itself
  */
@@ -327,7 +361,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	const char *interpreter;
 	struct file *interp_file;
 	struct binfmt_misc *misc;
-	bool preserve_argv0;
+	unsigned long flags;
 	int retval;
 
 	misc = current_binfmt_misc();
@@ -346,32 +380,14 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (IS_ERR(interpreter))
 		return PTR_ERR(interpreter);
 
-	/*
-	 * The invocation flags are fixed at registration for a static handler
-	 * and chosen per exec by the load program, via bpf_binprm_set_flags(),
-	 * for a bpf one.
-	 */
-	if (test_bit(MISC_FMT_BPF_BIT, &fmt->flags)) {
-		u64 f = bprm->bpf_flags;
-
-		/* Clear so it can't accumulate into a nested interpreter level. */
-		bprm->bpf_flags = 0;
-
-		preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0;
-		if (f & BPF_BINPRM_CREDENTIALS)
-			bprm->execfd_creds = 1;
-		if (f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD))
-			bprm->have_execfd = 1;
-	} else {
-		preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0;
-		if (fmt->flags & MISC_FMT_CREDENTIALS)
-			bprm->execfd_creds = 1;
-		if (fmt->flags & MISC_FMT_OPEN_BINARY)
-			bprm->have_execfd = 1;
-	}
+	flags = entry_invocation_flags(fmt, bprm);
+	if (flags & MISC_FMT_CREDENTIALS)
+		bprm->execfd_creds = 1;
+	if (flags & MISC_FMT_OPEN_BINARY)
+		bprm->have_execfd = 1;
 
 	/* The entry's own choice - not one accumulated from an earlier level. */
-	if (preserve_argv0) {
+	if (flags & MISC_FMT_PRESERVE_ARGV0) {
 		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
 	} else {
 		retval = remove_arg_zero(bprm);

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 05/21] binfmt_misc: split out entry_open_interpreter()
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (3 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 04/21] binfmt_misc: normalize the per-exec invocation flags Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 06/21] binfmt_misc: split out build_interp_argv() Christian Brauner
                   ` (15 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Opening the interpreter is a property of the matched entry: an 'F'
entry hands out a clone of the file it pre-opened at registration
time, any other entry opens the selected path. Give that its own
helper instead of an if/else in the middle of load_misc_binary(), and
let it fail early rather than carrying an ERR_PTR through the
successful branch.

No functional change.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_misc.c | 44 +++++++++++++++++++++++++++++++-------------
 1 file changed, 31 insertions(+), 13 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 7a6bbeb8a58c..63c10066e3a6 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -352,6 +352,36 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
 	return flags;
 }
 
+/**
+ * entry_open_interpreter - open the entry's interpreter for execution
+ * @e: matched binary type handler
+ * @interpreter: the interpreter selected for this exec
+ *
+ * An 'F' entry hands out a clone of the file it pre-opened at registration,
+ * any other entry opens the selected path.
+ *
+ * Return: the opened interpreter on success, an ERR_PTR on failure
+ */
+static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,
+					   const char *interpreter)
+{
+	struct file *interp_file __free(fput) = NULL;
+	int retval;
+
+	if (!(e->flags & MISC_FMT_OPEN_FILE))
+		return open_exec(interpreter);
+
+	interp_file = file_clone_open(e->interp_file);
+	if (IS_ERR(interp_file))
+		return interp_file;
+
+	retval = exe_file_deny_write_access(interp_file);
+	if (retval)
+		return ERR_PTR(retval);
+
+	return no_free_ptr(interp_file);
+}
+
 /*
  * the loader itself
  */
@@ -426,19 +456,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (retval < 0)
 		return retval;
 
-	if (fmt->flags & MISC_FMT_OPEN_FILE) {
-		interp_file = file_clone_open(fmt->interp_file);
-		if (!IS_ERR(interp_file)) {
-			int err = exe_file_deny_write_access(interp_file);
-
-			if (err) {
-				fput(interp_file);
-				interp_file = ERR_PTR(err);
-			}
-		}
-	} else {
-		interp_file = open_exec(interpreter);
-	}
+	interp_file = entry_open_interpreter(fmt, interpreter);
 	if (IS_ERR(interp_file))
 		return PTR_ERR(interp_file);
 

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 06/21] binfmt_misc: split out build_interp_argv()
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (4 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 05/21] binfmt_misc: split out entry_open_interpreter() Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 07/21] exec: release the replaced file with do_close_execat() Christian Brauner
                   ` (14 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Building the interpreter's argument vector is the bulk of
load_misc_binary() and the one part of it that is specific to the
classic dispatch. Move it into its own helper so what remains reads as
what it is: pick a handler, pick an interpreter, build the invocation,
open it.

No functional change.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_misc.c | 84 ++++++++++++++++++++++++++++++++++++--------------------
 1 file changed, 54 insertions(+), 30 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 63c10066e3a6..f0ccc5d000f6 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -382,40 +382,23 @@ static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,
 	return no_free_ptr(interp_file);
 }
 
-/*
- * the loader itself
+/**
+ * build_interp_argv - splice the interpreter invocation into the argv
+ * @bprm: binary that is being executed
+ * @interpreter: the interpreter selected for this exec
+ * @flags: invocation flags in effect for this exec
+ *
+ * The interpreter becomes argv[0] and the binary its last argument, with an
+ * optional staged argument in between. The caller's argv[0] is dropped
+ * unless 'P' keeps it.
+ *
+ * Return: 0 on success, a negative error code on failure
  */
-static int load_misc_binary(struct linux_binprm *bprm)
+static int build_interp_argv(struct linux_binprm *bprm, const char *interpreter,
+			     unsigned long flags)
 {
-	struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL;
-	const char *interpreter;
-	struct file *interp_file;
-	struct binfmt_misc *misc;
-	unsigned long flags;
 	int retval;
 
-	misc = current_binfmt_misc();
-	if (!READ_ONCE(misc->enabled))
-		return -ENOEXEC;
-
-	fmt = get_binfmt_handler(misc, bprm);
-	if (!fmt)
-		return -ENOEXEC;
-
-	/* Need to be able to load the file after exec */
-	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
-		return -ENOENT;
-
-	interpreter = entry_select_interpreter(fmt, bprm);
-	if (IS_ERR(interpreter))
-		return PTR_ERR(interpreter);
-
-	flags = entry_invocation_flags(fmt, bprm);
-	if (flags & MISC_FMT_CREDENTIALS)
-		bprm->execfd_creds = 1;
-	if (flags & MISC_FMT_OPEN_BINARY)
-		bprm->have_execfd = 1;
-
 	/* The entry's own choice - not one accumulated from an earlier level. */
 	if (flags & MISC_FMT_PRESERVE_ARGV0) {
 		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
@@ -451,6 +434,47 @@ static int load_misc_binary(struct linux_binprm *bprm)
 		return retval;
 	bprm->argc++;
 
+	return 0;
+}
+
+/*
+ * the loader itself
+ */
+static int load_misc_binary(struct linux_binprm *bprm)
+{
+	struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL;
+	const char *interpreter;
+	struct file *interp_file;
+	struct binfmt_misc *misc;
+	unsigned long flags;
+	int retval;
+
+	misc = current_binfmt_misc();
+	if (!READ_ONCE(misc->enabled))
+		return -ENOEXEC;
+
+	fmt = get_binfmt_handler(misc, bprm);
+	if (!fmt)
+		return -ENOEXEC;
+
+	/* Need to be able to load the file after exec */
+	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
+		return -ENOENT;
+
+	interpreter = entry_select_interpreter(fmt, bprm);
+	if (IS_ERR(interpreter))
+		return PTR_ERR(interpreter);
+
+	flags = entry_invocation_flags(fmt, bprm);
+	if (flags & MISC_FMT_CREDENTIALS)
+		bprm->execfd_creds = 1;
+	if (flags & MISC_FMT_OPEN_BINARY)
+		bprm->have_execfd = 1;
+
+	retval = build_interp_argv(bprm, interpreter, flags);
+	if (retval)
+		return retval;
+
 	/* Update interp in case binfmt_script needs it. */
 	retval = bprm_change_interp(interpreter, bprm);
 	if (retval < 0)

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 07/21] exec: release the replaced file with do_close_execat()
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (5 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 06/21] binfmt_misc: split out build_interp_argv() Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 08/21] exec: add AT_FLAGS_TRANSPARENT_INTERP Christian Brauner
                   ` (13 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

When the format search stages an interpreter exec_binprm() swaps it
in and releases the file it replaces. Dropping the write denial the
open took is done manually ahead of both release paths. The one path
that keeps the file silently relies on it not being called.

Let's just use do_close_execat() on the two paths that release the file
and drop the denial explicitly on the one that does not.

No functional change.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/exec.c | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/fs/exec.c b/fs/exec.c
index 2f50aa039827..6e540f4b43d8 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1741,15 +1741,17 @@ static int exec_binprm(struct linux_binprm *bprm)
 		bprm->file = bprm->interpreter;
 		bprm->interpreter = NULL;
 
-		exe_file_allow_write_access(exec);
 		if (unlikely(bprm->have_execfd)) {
 			if (bprm->executable) {
-				fput(exec);
+				do_close_execat(exec);
 				return -ENOEXEC;
 			}
+			/* Only the reference is kept, for AT_EXECFD. */
+			exe_file_allow_write_access(exec);
 			bprm->executable = exec;
-		} else
-			fput(exec);
+		} else {
+			do_close_execat(exec);
+		}
 	}
 
 	audit_bprm(bprm);

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 08/21] exec: add AT_FLAGS_TRANSPARENT_INTERP
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (6 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 07/21] exec: release the replaced file with do_close_execat() Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 09/21] exec: label mm->exe_file with the binary for a transparent dispatch Christian Brauner
                   ` (12 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

A transparent binfmt_misc dispatch hands the binary to the interpreter
through AT_EXECFD and leaves the argument vector exactly as the caller
built it. The loader on the receiving end has to know which contract it
got.

On the classic 'O'/'C' entries the binary's path is spliced into the
argument vector and the loader consumes arguments. In transparent mode
nothing was spliced and argv belongs entirely to the program. This
cannot be inferred from AT_EXECFD alone. Raise a new AT_FLAGS bit
following the AT_FLAGS_PRESERVE_ARGV0 precedent added for qemu-user in
commit 2347961b11d4 ("binfmt_misc: pass binfmt_misc flags to the
interpreter").

The bit also announces that mm->exe_file names the binary rather than
the interpreter (added in the next commit). A loader that sees the bit
may finish the identity polish by fixing up AT_PHDR/AT_ENTRY/AT_BASE in
saved_auxv and fix the code/data markers via one uncapped PR_SET_MM_MAP
once it has mapped the binary. I've got glibc patches for this as well
but it's useful for any loader.

BINPRM_FLAGS_TRANSPARENT_INTERP carries the mode from binfmt_misc to
the ELF loaders. Both create_elf_tables() variants translate it into
the aux vector bit. Nothing sets the bprm flag yet.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_elf.c              | 5 ++++-
 fs/binfmt_elf_fdpic.c        | 5 ++++-
 include/linux/binfmts.h      | 4 ++++
 include/uapi/linux/binfmts.h | 7 +++++++
 4 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index 16a56b6b3f6c..e44230242f3a 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -254,7 +254,10 @@ create_elf_tables(struct linux_binprm *bprm, const struct elfhdr *exec,
 	NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
 	NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
 	NEW_AUX_ENT(AT_BASE, interp_load_addr);
-	if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0)
+	/* Transparency preserves the whole argv, argv[0] included. */
+	if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP)
+		flags |= AT_FLAGS_TRANSPARENT_INTERP;
+	else if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0)
 		flags |= AT_FLAGS_PRESERVE_ARGV0;
 	NEW_AUX_ENT(AT_FLAGS, flags);
 	NEW_AUX_ENT(AT_ENTRY, e_entry);
diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c
index 7e3108489c83..3306d21ab738 100644
--- a/fs/binfmt_elf_fdpic.c
+++ b/fs/binfmt_elf_fdpic.c
@@ -645,7 +645,10 @@ static int create_elf_fdpic_tables(struct linux_binprm *bprm,
 	NEW_AUX_ENT(AT_PHENT,	sizeof(struct elf_phdr));
 	NEW_AUX_ENT(AT_PHNUM,	exec_params->hdr.e_phnum);
 	NEW_AUX_ENT(AT_BASE,	interp_params->elfhdr_addr);
-	if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0)
+	/* Transparency preserves the whole argv, argv[0] included. */
+	if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP)
+		flags |= AT_FLAGS_TRANSPARENT_INTERP;
+	else if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0)
 		flags |= AT_FLAGS_PRESERVE_ARGV0;
 	NEW_AUX_ENT(AT_FLAGS,	flags);
 	NEW_AUX_ENT(AT_ENTRY,	exec_params->entry_addr);
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index 03e1794b5cbb..d4f64d143d1a 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -93,6 +93,10 @@ struct linux_binprm {
 #define BINPRM_FLAGS_PRESERVE_ARGV0_BIT 3
 #define BINPRM_FLAGS_PRESERVE_ARGV0 (1 << BINPRM_FLAGS_PRESERVE_ARGV0_BIT)
 
+/* binfmt_misc dispatched to the interpreter transparently */
+#define BINPRM_FLAGS_TRANSPARENT_INTERP_BIT 4
+#define BINPRM_FLAGS_TRANSPARENT_INTERP (1 << BINPRM_FLAGS_TRANSPARENT_INTERP_BIT)
+
 /*
  * This structure defines the functions that are used to load the binary formats that
  * linux accepts.
diff --git a/include/uapi/linux/binfmts.h b/include/uapi/linux/binfmts.h
index c6f9450efc12..aafc07d78b80 100644
--- a/include/uapi/linux/binfmts.h
+++ b/include/uapi/linux/binfmts.h
@@ -22,4 +22,11 @@ struct pt_regs;
 #define AT_FLAGS_PRESERVE_ARGV0_BIT 0
 #define AT_FLAGS_PRESERVE_ARGV0 (1 << AT_FLAGS_PRESERVE_ARGV0_BIT)
 
+/*
+ * The interpreter runs transparently: the argument vector and the exe
+ * link belong to the binary passed in AT_EXECFD.
+ */
+#define AT_FLAGS_TRANSPARENT_INTERP_BIT 1
+#define AT_FLAGS_TRANSPARENT_INTERP (1 << AT_FLAGS_TRANSPARENT_INTERP_BIT)
+
 #endif /* _UAPI_LINUX_BINFMTS_H */

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 09/21] exec: label mm->exe_file with the binary for a transparent dispatch
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (7 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 08/21] exec: add AT_FLAGS_TRANSPARENT_INTERP Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 10/21] binfmt_misc: add transparent interpreter dispatch Christian Brauner
                   ` (11 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

When binfmt_misc dispatches a binary to an interpreter, the interpreter
becomes bprm->file and begin_new_exec() labels mm->exe_file with it. For
wine or qemu-user that is the point. For the transparent mode it
defeats the point. The interpreter is an implementation detail and the
process's identity is the binary. Relocatable programs that locate
themselves via /proc/self/exe find the dynamic linker instead [1].

Userspace cannot repair this after the fact. The exe link is the one
capability-gated field of PR_SET_MM_MAP, deliberately, because an
uncapped exe swap lets any ptrace-capable process masquerade as an
arbitrary executable.

bprm->executable is the file execve() access-checked and kept open for
AT_EXECFD. It is already the file would_dump() bases the dumpability
decision on and the file bprm->execfd_creds derives credentials from.
Label mm->exe_file with it when the dispatch is transparent and the
identity is correct from the start. The label names precisely the file
the caller passed to execve().

Write-denial moves along with the label. The result is exact parity with
a direct execution. A concurrently written binary fails execve() with
-ETXTBSY at open and a running one cannot be opened for writing. The
interpreter consequently is not exe-pinned and matches the role it has
in a native PT_INTERP exec.

Nothing sets BINPRM_FLAGS_TRANSPARENT_INTERP yet; the transparent
dispatch machinery in binfmt_misc follows and raises it from birth, so
the label and the aux vector bit that announces it appear together.

Link: https://inbox.sourceware.org/libc-alpha/87ik6fymha.fsf@oldenburg.str.redhat.com [1]
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/exec.c               | 33 ++++++++++++++++++++++++++++-----
 include/linux/binfmts.h |  4 +++-
 2 files changed, 31 insertions(+), 6 deletions(-)

diff --git a/fs/exec.c b/fs/exec.c
index 6e540f4b43d8..45d416994682 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1101,6 +1101,17 @@ void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec)
 	perf_event_comm(tsk, exec);
 }
 
+/*
+ * The file the process presents as: its exe link and comm. A transparent
+ * dispatch presents as the binary, which is bprm->executable.
+ */
+static struct file *bprm_identity_file(const struct linux_binprm *bprm)
+{
+	if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP)
+		return bprm->executable;
+	return bprm->file;
+}
+
 /*
  * Calling this is the point of no return. None of the failures will be
  * seen by userspace since either the process is already taking a fatal
@@ -1157,7 +1168,7 @@ int begin_new_exec(struct linux_binprm * bprm)
 	 * not visible until then. Doing it here also ensures
 	 * we don't race against replace_mm_exe_file().
 	 */
-	retval = set_mm_exe_file(bprm->mm, bprm->file);
+	retval = set_mm_exe_file(bprm->mm, bprm_identity_file(bprm));
 	if (retval)
 		goto out;
 
@@ -1247,6 +1258,8 @@ int begin_new_exec(struct linux_binprm * bprm)
 	 * Let's fix it up to be something reasonable.
 	 */
 	if (bprm->comm_from_dentry) {
+		struct file *comm_file = bprm_identity_file(bprm);
+
 		/*
 		 * Hold RCU lock to keep the name from being freed behind our back.
 		 * Use acquire semantics to make sure the terminating NUL from
@@ -1256,7 +1269,7 @@ int begin_new_exec(struct linux_binprm * bprm)
 		 * detecting a concurrent rename and just want a terminated name.
 		 */
 		rcu_read_lock();
-		__set_task_comm(me, smp_load_acquire(&bprm->file->f_path.dentry->d_name.name),
+		__set_task_comm(me, smp_load_acquire(&comm_file->f_path.dentry->d_name.name),
 				true);
 		rcu_read_unlock();
 	} else {
@@ -1300,6 +1313,9 @@ int begin_new_exec(struct linux_binprm * bprm)
 		retval = FD_ADD(0, bprm->executable);
 		if (retval < 0)
 			goto out_unlock;
+		/* mm->exe_file carries its own write denial now. */
+		if (bprm->executable_denied)
+			exe_file_allow_write_access(bprm->executable);
 		bprm->executable = NULL;
 		bprm->execfd = retval;
 	}
@@ -1419,8 +1435,12 @@ static void free_bprm(struct linux_binprm *bprm)
 	if (bprm->old_mm)
 		exec_mm_put_old(bprm->old_mm);
 	do_close_execat(bprm->file);
-	if (bprm->executable)
+	if (bprm->executable) {
+		/* A transparent dispatch still holds the write denial. */
+		if (bprm->executable_denied)
+			exe_file_allow_write_access(bprm->executable);
 		fput(bprm->executable);
+	}
 	/* If a binfmt changed the interp, free it. */
 	if (bprm->interp != bprm->filename)
 		kfree(bprm->interp);
@@ -1746,8 +1766,11 @@ static int exec_binprm(struct linux_binprm *bprm)
 				do_close_execat(exec);
 				return -ENOEXEC;
 			}
-			/* Only the reference is kept, for AT_EXECFD. */
-			exe_file_allow_write_access(exec);
+			/* A transparent dispatch keeps the denial for mm->exe_file. */
+			if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP)
+				bprm->executable_denied = 1;
+			else
+				exe_file_allow_write_access(exec);
 			bprm->executable = exec;
 		} else {
 			do_close_execat(exec);
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index d4f64d143d1a..ba5037b69866 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -59,7 +59,9 @@ struct linux_binprm {
 		 * Set by user space to check executability according to the
 		 * caller's environment.
 		 */
-		is_check:1;
+		is_check:1,
+		/* bprm->executable holds a write denial still to be released. */
+		executable_denied:1;
 	struct file *executable; /* Executable to pass to the interpreter */
 	struct file *interpreter;
 	struct file *file;

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 10/21] binfmt_misc: add transparent interpreter dispatch
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (8 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 09/21] exec: label mm->exe_file with the binary for a transparent dispatch Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 11/21] binfmt_misc: add a static transparent flag 'T' Christian Brauner
                   ` (10 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

A binfmt_misc interpreter is visible to the binary it runs. argv[0]
becomes the interpreter path and the binary's path is appended as an
argument and /proc/pid/cmdline shows both. For wine or qemu-user that is
the point. For a per-binary loader the interpreter is an implementation
detail of running the binary that has no business in the argument
vector. And a binary handed to execveat() as an O_CLOEXEC fd without a
usable path cannot be run through binfmt_misc at all. The interpreter
would have no path to open the binary by.

Add the dispatch machinery for a transparent mode. The binary is handed
to the interpreter through AT_EXECFD. The argument vector and
bprm->interp are left exactly as the caller set them. argv[0] and
/proc/pid/cmdline look like a direct execution of the binary.

The interpreter loads the binary from AT_EXECFD for this. A relocatable
loader can and glibc's ld.so is gaining AT_EXECFD support [1]. A staged
interpreter argument is dropped so no argv slot is built for it to land
in.

The transparent branch raises BINPRM_FLAGS_TRANSPARENT_INTERP. A
dispatch through it labels mm->exe_file with the binary and raises
AT_FLAGS_TRANSPARENT_INTERP next to AT_EXECFD. The aux vector bit is the
loader's hint to retarget saved_auxv and the statistics markers to the
binary, which is only correct while the exe link names the binary too.

The inaccessible-path bail moves after handler selection and into the
path-building branch. A transparent interpreter takes the binary from
AT_EXECFD instead of a path, so the restriction does not apply to it
and the O_CLOEXEC execveat() case above can work. Nothing can take the
transparent branch yet.

Link: https://inbox.sourceware.org/libc-alpha/20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org [1]
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_misc.c | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index f0ccc5d000f6..239cca389e0e 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -50,6 +50,7 @@ enum binfmt_misc_entry_flags {
 	MISC_FMT_OPEN_BINARY	= (1U << 30),
 	MISC_FMT_CREDENTIALS	= (1U << 29),
 	MISC_FMT_OPEN_FILE	= (1U << 28),
+	MISC_FMT_TRANSPARENT	= (1U << 27),
 };
 
 /**
@@ -399,6 +400,10 @@ static int build_interp_argv(struct linux_binprm *bprm, const char *interpreter,
 {
 	int retval;
 
+	/* The interpreter has to be able to load the binary by path. */
+	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
+		return -ENOENT;
+
 	/* The entry's own choice - not one accumulated from an earlier level. */
 	if (flags & MISC_FMT_PRESERVE_ARGV0) {
 		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
@@ -457,10 +462,6 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (!fmt)
 		return -ENOEXEC;
 
-	/* Need to be able to load the file after exec */
-	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
-		return -ENOENT;
-
 	interpreter = entry_select_interpreter(fmt, bprm);
 	if (IS_ERR(interpreter))
 		return PTR_ERR(interpreter);
@@ -471,11 +472,17 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (flags & MISC_FMT_OPEN_BINARY)
 		bprm->have_execfd = 1;
 
-	retval = build_interp_argv(bprm, interpreter, flags);
-	if (retval)
-		return retval;
+	if (flags & MISC_FMT_TRANSPARENT) {
+		/* No argv is built for a staged argument to land in. */
+		kfree(bprm->bpf_interp_arg);
+		bprm->bpf_interp_arg = NULL;
+	} else {
+		retval = build_interp_argv(bprm, interpreter, flags);
+		if (retval)
+			return retval;
+	}
 
-	/* Update interp in case binfmt_script needs it. */
+	/* Update interp for the next round; kernel-internal, not user-visible. */
 	retval = bprm_change_interp(interpreter, bprm);
 	if (retval < 0)
 		return retval;
@@ -484,6 +491,10 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (IS_ERR(interp_file))
 		return PTR_ERR(interp_file);
 
+	/* Raise only past the last failure, or an -ENOEXEC decline leaks it. */
+	if (flags & MISC_FMT_TRANSPARENT)
+		bprm->interp_flags |= BINPRM_FLAGS_TRANSPARENT_INTERP;
+
 	bprm->interpreter = interp_file;
 	return 0;
 }

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 11/21] binfmt_misc: add a static transparent flag 'T'
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (9 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 10/21] binfmt_misc: add transparent interpreter dispatch Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 12/21] binfmt_misc: let a bpf handler run the interpreter transparently Christian Brauner
                   ` (9 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Let a registration opt into transparent dispatch. The 'T' flag lets a
matched binary keep its argument vector and is sent to the interpreter
through AT_EXECFD. The process's identity is the binary's.

'T' implies 'O' exactly like 'C' does. 'P' is rejected in combination
with it. Transparency preserves the whole argument vector so there is
nothing left for 'P' to say. 'C' remains an independent choice and 'F'
keeps working. A pre-opened interpreter is orthogonal to how the binary
is handed over.

Like the other flag characters 'T' cannot be used as the field
delimiter. The flag scan would run off the registration buffer.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst | 10 ++++++++++
 fs/binfmt_misc.c                          |  6 ++++++
 2 files changed, 16 insertions(+)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index c76381a4bcad..c370c72c7dbe 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -90,6 +90,16 @@ Here is what the fields mean:
 	    emulation is installed and uses the opened image to spawn the
 	    emulator, meaning it is always available once installed,
 	    regardless of how the environment changes.
+      ``T`` - transparent
+            Run the interpreter transparently. The binary is handed to
+            the interpreter through ``AT_EXECFD`` (``T`` implies ``O``),
+            the argument vector is left exactly as the caller built it
+            and the kernel labels ``/proc/pid/exe`` with the binary
+            instead of the interpreter. The interpreter has to load the
+            binary from ``AT_EXECFD`` and follow the
+            ``AT_FLAGS_TRANSPARENT_INTERP`` contract. Combining ``T``
+            with ``P`` is rejected: transparency preserves the whole
+            argument vector, argv[0] included.
 
 
 There are some restrictions:
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 239cca389e0e..ed5ffb330749 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -72,6 +72,7 @@ static const struct binfmt_misc_flag misc_flags[] = {
 	{ 'O', MISC_FMT_OPEN_BINARY,	0,			"open binary"			},
 	{ 'C', MISC_FMT_CREDENTIALS,	MISC_FMT_OPEN_BINARY,	"credentials from the binary"	},
 	{ 'F', MISC_FMT_OPEN_FILE,	0,			"open interpreter file now"	},
+	{ 'T', MISC_FMT_TRANSPARENT,	MISC_FMT_OPEN_BINARY,	"transparent"			},
 };
 
 /* Look up a flag character, NULL if @c is not one. */
@@ -763,6 +764,11 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 	if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags)
 		return ERR_PTR(-EINVAL);
 
+	/* Transparency preserves the whole argv, argv[0] included. */
+	if ((e->flags & MISC_FMT_TRANSPARENT) &&
+	    (e->flags & MISC_FMT_PRESERVE_ARGV0))
+		return ERR_PTR(-EINVAL);
+
 	if (*p == '\n')
 		p++;
 	if (p != buf + count)

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 12/21] binfmt_misc: let a bpf handler run the interpreter transparently
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (10 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 11/21] binfmt_misc: add a static transparent flag 'T' Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 13/21] selftests/exec: convert the binfmt_misc bpf test to the kselftest harness Christian Brauner
                   ` (8 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Expose transparent mode 'T' to the bpf handler via a new
BPF_BINPRM_TRANSPARENT flag. A bpf handler can decide per binary whether
the dispatch is transparent. This way users may choose a native-looking
loader for one binary and a visible wrapper invocation for the next.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst | 14 ++++++++++++--
 fs/binfmt_misc.c                          |  2 ++
 fs/binfmt_misc_bpf.c                      | 13 ++++++++++---
 include/linux/binfmt_misc.h               |  4 ++++
 4 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index c370c72c7dbe..b27ad31847ab 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -185,8 +185,8 @@ interpreter and the binary, exactly like the optional argument of a ``#!``
 interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's
 ``#!`` path and needs to preserve the argument that followed it.
 
-The invocation flags a static entry fixes at registration - ``P``, ``C``
-and ``O`` - are per-exec choices for a bpf handler, made by the ``load``
+The invocation flags a static entry fixes at registration - ``P``, ``C``,
+``O`` and ``T`` - are per-exec choices for a bpf handler, made by the ``load``
 program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can
 decide them differently for each binary it handles:
 
@@ -198,6 +198,16 @@ decide them differently for each binary it handles:
 - ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and
   passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so
   the interpreter can run binaries it could not open by path.
+- ``BPF_BINPRM_TRANSPARENT`` runs the interpreter transparently (the ``T``
+  flag): the binary is handed over through ``AT_EXECFD`` as
+  with ``BPF_BINPRM_EXECFD``, but the argument vector is also left as the
+  caller passed it. An interpreter that loads the binary from ``AT_EXECFD``
+  then appears in ``argv[0]`` and ``/proc/pid/cmdline`` as a direct
+  execution of the binary. ``BPF_BINPRM_PRESERVE_ARGV0`` is rejected in
+  combination with it, just as ``P`` is with ``T``. It also lets a handler
+  run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor to
+  ``execveat()``, which a path-splicing dispatch cannot: the interpreter
+  has no path by which to open it.
 
 Because these are program choices, a ``B`` entry carries no flags in the
 register string; ``F`` (pre-open a fixed interpreter) has no meaning for it.
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index ed5ffb330749..1cd30dec3fab 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -350,6 +350,8 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
 		flags |= MISC_FMT_OPEN_BINARY;
 	if (bpf_flags & BPF_BINPRM_CREDENTIALS)
 		flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY;
+	if (bpf_flags & BPF_BINPRM_TRANSPARENT)
+		flags |= MISC_FMT_TRANSPARENT | MISC_FMT_OPEN_BINARY;
 
 	return flags;
 }
diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c
index 00c787e8bdcc..d06cacc5c8c7 100644
--- a/fs/binfmt_misc_bpf.c
+++ b/fs/binfmt_misc_bpf.c
@@ -178,15 +178,22 @@ __bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
  * O flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0],
  * BPF_BINPRM_CREDENTIALS computes credentials from the binary, and
  * BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD.
- * Calling it again replaces the flags, passing zero clears them again.
+ * BPF_BINPRM_TRANSPARENT additionally leaves the argument vector untouched,
+ * making the exec look like a direct execution of the binary. Calling it
+ * again replaces the flags, passing zero clears them again.
  *
- * Return: 0 on success, -EINVAL if @flags contains an unknown bit
+ * Return: 0 on success, -EINVAL if @flags contains an unknown bit or an
+ * invalid combination
  */
 __bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm,
 				     enum bpf_binprm_flags flags)
 {
 	if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS |
-		      BPF_BINPRM_EXECFD))
+		      BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT))
+		return -EINVAL;
+
+	/* Transparency preserves the whole argv, argv[0] included. */
+	if ((flags & BPF_BINPRM_TRANSPARENT) && (flags & BPF_BINPRM_PRESERVE_ARGV0))
 		return -EINVAL;
 
 	bprm->bpf_flags = flags;
diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h
index d3112a00cc19..26da749391b4 100644
--- a/include/linux/binfmt_misc.h
+++ b/include/linux/binfmt_misc.h
@@ -16,6 +16,9 @@ struct user_namespace;
  * @BPF_BINPRM_CREDENTIALS: compute credentials from the binary; implies execfd
  *                          (like the 'C' flag)
  * @BPF_BINPRM_EXECFD: pass the binary via AT_EXECFD (like the 'O' flag)
+ * @BPF_BINPRM_TRANSPARENT: leave argv untouched, the interpreter takes the
+ *                          binary from AT_EXECFD (like the 'T' flag); implies
+ *                          execfd, excludes preserve-argv0
  *
  * Set from a load program with bpf_binprm_set_flags(). Unlike a static entry,
  * a bpf handler chooses these per exec rather than once at registration.
@@ -24,6 +27,7 @@ enum bpf_binprm_flags {
 	BPF_BINPRM_PRESERVE_ARGV0	= (1ULL << 0),
 	BPF_BINPRM_CREDENTIALS		= (1ULL << 1),
 	BPF_BINPRM_EXECFD		= (1ULL << 2),
+	BPF_BINPRM_TRANSPARENT		= (1ULL << 3),
 };
 
 /**

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 13/21] selftests/exec: convert the binfmt_misc bpf test to the kselftest harness
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (11 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 12/21] binfmt_misc: let a bpf handler run the interpreter transparently Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode Christian Brauner
                   ` (7 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

The test reports its own pass and fail lines, returns a bare 4 for
KSFT_SKIP and runs both cases in one process, so a failure in the first
takes the second with it. It also open-codes the register, unregister,
file-copy and mount helpers that the tests for the upcoming transparent
and loader dispatch modes need again.

Convert it to the kselftest harness: a fixture for the common setup and
teardown, one TEST_F per case so each is reported and isolated
separately, and SKIP() for the root, BTF and binfmt_misc preconditions.
Move the helpers to a shared header on the way, with the register
helper preserving the write's errno so a caller can tell a rejected
flag combination (EINVAL) from a kernel that does not know the flag at
all. The synthetic ELF header gains an e_machine argument and uses the
elf.h constants instead of open-coded numbers.

No change in what is tested.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/exec/Makefile             |   6 +-
 tools/testing/selftests/exec/binfmt_misc_bpf.c    | 195 ++++++++--------------
 tools/testing/selftests/exec/binfmt_misc_common.h |  98 +++++++++++
 3 files changed, 168 insertions(+), 131 deletions(-)

diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index ec66c1fecfc0..fb5ca59af51d 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -44,6 +44,8 @@ endif
 EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx*	\
 	       $(OUTPUT)/S_I*.test
 
+LOCAL_HDRS += binfmt_misc_common.h
+
 include ../lib.mk
 
 CHECK_EXEC_SAMPLES := $(top_srcdir)/samples/check-exec
@@ -91,7 +93,7 @@ $(OUTPUT)/vmlinux.h:
 $(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h
 	$(CLANG) -g -O2 -target bpf -mcpu=v3 $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@
 
-$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c
+$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
 	$(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@
 
 $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
@@ -102,4 +104,4 @@ $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
 $(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c
 	$(CC) $(CFLAGS) $(LDFLAGS) -Wl,--dynamic-linker,'$$ORIGIN/binfmt_bpf_interp' $< -o $@
 
-EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/bpf_interp.bpf.o $(OUTPUT)/nix_origin.bpf.o
+EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/*.bpf.o
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index cb89d2766fe2..7954c6274623 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -23,66 +23,40 @@
  * program's chosen interpreter actually ran.
  */
 #define _GNU_SOURCE
+#include <elf.h>
+#include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <fcntl.h>
-#include <libgen.h>
-#include <sys/mount.h>
 #include <sys/stat.h>
 
 #include <bpf/btf.h>
 #include <bpf/libbpf.h>
 
+#include "binfmt_misc_common.h"
+#include "../kselftest_harness.h"
+
 #define INTERP_PATH	"/tmp/binfmt_bpf_interp"
 #define AARCH64_PATH	"/tmp/binfmt_bpf_aarch64"
 #define RELOC_DIR	"/tmp/binfmt_reloc"
-#define BINFMT_REG	"/proc/sys/fs/binfmt_misc/register"
 #define EXPECT		"BPF_INTERP_RAN"
 
-static char testdir[512]; /* directory holding this test's built artifacts */
-
-static int copy_file(const char *src, const char *dst)
-{
-	char buf[4096];
-	int in, out;
-	ssize_t n;
-
-	in = open(src, O_RDONLY);
-	if (in < 0)
-		return -1;
-	out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755);
-	if (out < 0) {
-		close(in);
-		return -1;
-	}
-	while ((n = read(in, buf, sizeof(buf))) > 0) {
-		if (write(out, buf, n) != n) {
-			close(in);
-			close(out);
-			return -1;
-		}
-	}
-	close(in);
-	close(out);
-	return n < 0 ? -1 : 0;
-}
-
-/* A minimal 64-bit little-endian aarch64 ELF header, padded to the read size. */
-static int create_fake_aarch64(const char *path)
+/* A minimal 64-bit little-endian ELF header, padded to the read size. */
+static int create_fake_elf(const char *path, unsigned short machine)
 {
 	unsigned char hdr[256] = {0};
 	int fd;
 
 	hdr[0] = 0x7f; hdr[1] = 'E'; hdr[2] = 'L'; hdr[3] = 'F';
-	hdr[4] = 2;			/* ELFCLASS64 */
-	hdr[5] = 1;			/* ELFDATA2LSB */
-	hdr[6] = 1;			/* EV_CURRENT */
-	hdr[16] = 2;			/* e_type = ET_EXEC */
-	hdr[18] = 183 & 0xff;		/* e_machine = EM_AARCH64 */
-	hdr[19] = (183 >> 8) & 0xff;
-	hdr[20] = 1;			/* e_version */
+	hdr[4] = ELFCLASS64;
+	hdr[5] = ELFDATA2LSB;
+	hdr[6] = EV_CURRENT;
+	hdr[16] = ET_EXEC;
+	hdr[18] = machine & 0xff;	/* e_machine, little-endian */
+	hdr[19] = machine >> 8;
+	hdr[20] = EV_CURRENT;
 
 	fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
 	if (fd < 0)
@@ -97,31 +71,10 @@ static int create_fake_aarch64(const char *path)
 
 static int register_entry(const char *name, const char *handler)
 {
-	char rule[128];
-	int fd;
-	ssize_t n;
+	char rule[PATH_MAX];
 
 	snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler);
-	fd = open(BINFMT_REG, O_WRONLY);
-	if (fd < 0)
-		return -1;
-	n = write(fd, rule, strlen(rule));
-	close(fd);
-	return n < 0 ? -1 : 0;
-}
-
-static void unregister_entry(const char *name)
-{
-	char path[128];
-	int fd;
-
-	snprintf(path, sizeof(path), "/proc/sys/fs/binfmt_misc/%s", name);
-	fd = open(path, O_WRONLY);
-	if (fd >= 0) {
-		if (write(fd, "-1", 2) < 0)
-			; /* best effort */
-		close(fd);
-	}
+	return write_reg(rule);
 }
 
 static int check_output(const char *cmd, const char *expected)
@@ -178,7 +131,7 @@ static int run_case(const char *objfile, const char *handler,
 		goto detach;
 	}
 	ret = check_output(target, expect);
-	unregister_entry(entry);
+	unregister(entry);
 detach:
 	bpf_link__destroy(link);
 close:
@@ -186,92 +139,76 @@ static int run_case(const char *objfile, const char *handler,
 	return ret;
 }
 
-int main(void)
+FIXTURE(bpf_handler) {
+	char obj[PATH_MAX];	/* struct_ops object of the case under test */
+};
+
+FIXTURE_SETUP(bpf_handler)
 {
-	char src[600], obj[600], appdst[600], interpdst[600];
-	char exe[512];
-	ssize_t n;
-	int fail = 0;
+	char src[PATH_MAX];
 	struct stat st;
 	struct btf *btf;
 
-	if (getuid() != 0) {
-		fprintf(stderr, "Skipping: test must be run as root\n");
-		return 4; /* KSFT_SKIP */
-	}
+	if (getuid() != 0)
+		SKIP(return, "test must be run as root");
 
 	/* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */
 	btf = btf__load_vmlinux_btf();
 	if (!btf || btf__find_by_name_kind(btf, "binfmt_misc_ops",
 					   BTF_KIND_STRUCT) < 0) {
-		fprintf(stderr,
-			"Skipping: no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)\n");
 		btf__free(btf);
-		return 4; /* KSFT_SKIP */
+		SKIP(return,
+		     "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)");
 	}
 	btf__free(btf);
 
-	n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
-	if (n < 0) {
-		perror("readlink");
-		return 1;
-	}
-	exe[n] = '\0';
-	snprintf(testdir, sizeof(testdir), "%s", dirname(exe));
-
 	if (stat("/sys/fs/bpf", &st) < 0)
 		mkdir("/sys/fs/bpf", 0755);
 	mount("bpf", "/sys/fs/bpf", "bpf", 0, NULL);
-	if (access(BINFMT_REG, F_OK) < 0)
-		mount("binfmt_misc", "/proc/sys/fs/binfmt_misc", "binfmt_misc", 0, NULL);
+	if (!binfmt_misc_available())
+		SKIP(return, "no binfmt_misc");
 
 	/* Shared test interpreter. */
-	snprintf(src, sizeof(src), "%s/binfmt_bpf_interp", testdir);
-	if (copy_file(src, INTERP_PATH)) {
-		fprintf(stderr, "cannot install %s\n", INTERP_PATH);
-		return 1;
-	}
+	ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0);
+	ASSERT_EQ(copy_file(src, INTERP_PATH), 0);
+}
 
-	/* Case 1: match a synthetic aarch64 header -> fixed interpreter. */
-	printf("[*] case 1: match aarch64 header -> program-chosen interpreter\n");
-	if (create_fake_aarch64(AARCH64_PATH)) {
-		fprintf(stderr, "cannot create %s\n", AARCH64_PATH);
-		return 1;
-	}
-	snprintf(obj, sizeof(obj), "%s/bpf_interp.bpf.o", testdir);
-	if (run_case(obj, "bpf_interp", "test_bpf_interp", AARCH64_PATH, EXPECT) == 0)
-		printf("[+] case 1 passed\n");
-	else {
-		printf("[-] case 1 FAILED\n");
-		fail = 1;
-	}
+FIXTURE_TEARDOWN(bpf_handler)
+{
+	unlink(INTERP_PATH);
+}
+
+/* The match program matches a synthetic header, the load program routes it. */
+TEST_F(bpf_handler, fixed_interpreter)
+{
+	ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+	ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+				"bpf_interp.bpf.o"), 0);
+	EXPECT_EQ(run_case(self->obj, "bpf_interp", "test_bpf_interp",
+			   AARCH64_PATH, EXPECT), 0);
 	unlink(AARCH64_PATH);
+}
+
+/* A "$ORIGIN/..." PT_INTERP resolved to an interpreter next to the binary. */
+TEST_F(bpf_handler, origin_relative_interpreter)
+{
+	char src[PATH_MAX], app[PATH_MAX], interp[PATH_MAX];
 
-	/* Case 2: $ORIGIN-relative PT_INTERP -> co-located interpreter. */
-	printf("[*] case 2: $ORIGIN interpreter resolved relative to the binary\n");
 	mkdir(RELOC_DIR, 0755);
-	snprintf(appdst, sizeof(appdst), "%s/app", RELOC_DIR);
-	snprintf(interpdst, sizeof(interpdst), "%s/binfmt_bpf_interp", RELOC_DIR);
-	snprintf(src, sizeof(src), "%s/binfmt_bpf_app", testdir);
-	if (copy_file(src, appdst) ||
-	    copy_file(INTERP_PATH, interpdst)) {
-		fprintf(stderr, "cannot set up %s\n", RELOC_DIR);
-		fail = 1;
-	} else {
-		snprintf(obj, sizeof(obj), "%s/nix_origin.bpf.o", testdir);
-		if (run_case(obj, "nix_origin", "test_bpf_origin", appdst, EXPECT) == 0)
-			printf("[+] case 2 passed\n");
-		else {
-			printf("[-] case 2 FAILED\n");
-			fail = 1;
-		}
-	}
-	unlink(appdst);
-	unlink(interpdst);
-	rmdir(RELOC_DIR);
-	unlink(INTERP_PATH);
+	snprintf(app, sizeof(app), "%s/app", RELOC_DIR);
+	snprintf(interp, sizeof(interp), "%s/binfmt_bpf_interp", RELOC_DIR);
+	ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_app"), 0);
+	ASSERT_EQ(copy_file(src, app), 0);
+	ASSERT_EQ(copy_file(INTERP_PATH, interp), 0);
+
+	ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+				"nix_origin.bpf.o"), 0);
+	EXPECT_EQ(run_case(self->obj, "nix_origin", "test_bpf_origin",
+			   app, EXPECT), 0);
 
-	if (!fail)
-		printf("[*] all binfmt_misc bpf cases passed\n");
-	return fail;
+	unlink(app);
+	unlink(interp);
+	rmdir(RELOC_DIR);
 }
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
new file mode 100644
index 000000000000..854c7a6d34e1
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -0,0 +1,98 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Helpers shared by the binfmt_misc selftests. */
+#ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
+#define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <unistd.h>
+
+#define BINFMT_DIR	"/proc/sys/fs/binfmt_misc"
+#define BINFMT_REG	BINFMT_DIR "/register"
+
+static inline int copy_file(const char *src, const char *dst)
+{
+	char buf[4096];
+	int in, out;
+	ssize_t n;
+
+	in = open(src, O_RDONLY);
+	if (in < 0)
+		return -1;
+	out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755);
+	if (out < 0) {
+		close(in);
+		return -1;
+	}
+	while ((n = read(in, buf, sizeof(buf))) > 0) {
+		if (write(out, buf, n) != n) {
+			close(in);
+			close(out);
+			return -1;
+		}
+	}
+	close(in);
+	close(out);
+	return n < 0 ? -1 : 0;
+}
+
+/* Write @rule to the register file, preserving the write's errno. */
+static inline int write_reg(const char *rule)
+{
+	int fd, saved;
+	ssize_t n;
+
+	fd = open(BINFMT_REG, O_WRONLY);
+	if (fd < 0)
+		return -1;
+	n = write(fd, rule, strlen(rule));
+	saved = errno;
+	close(fd);
+	errno = saved;
+	return n < 0 ? -1 : 0;
+}
+
+static inline void unregister(const char *name)
+{
+	char path[PATH_MAX];
+	int fd;
+
+	snprintf(path, sizeof(path), BINFMT_DIR "/%s", name);
+	fd = open(path, O_WRONLY);
+	if (fd >= 0) {
+		if (write(fd, "-1", 2) < 0)
+			; /* best effort */
+		close(fd);
+	}
+}
+
+/* Mount binfmt_misc unless it already is, and report whether it is usable. */
+static inline bool binfmt_misc_available(void)
+{
+	if (access(BINFMT_REG, F_OK) < 0)
+		mount("binfmt_misc", BINFMT_DIR, "binfmt_misc", 0, NULL);
+	return access(BINFMT_REG, F_OK) == 0;
+}
+
+/* Absolute path of @name in the directory this test was built into. */
+static inline int artifact_path(char *out, size_t sz, const char *name)
+{
+	char exe[PATH_MAX];
+	ssize_t n;
+
+	n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
+	if (n < 0)
+		return -1;
+	exe[n] = '\0';
+	if ((size_t)snprintf(out, sz, "%s/%s", dirname(exe), name) >= sz)
+		return -1;
+	return 0;
+}
+
+#endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (12 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 13/21] selftests/exec: convert the binfmt_misc bpf test to the kselftest harness Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20 11:44   ` sashiko-bot
  2026-07-20  9:33 ` [PATCH 15/21] binfmt_misc: document the transparent identity contract Christian Brauner
                   ` (6 subsequent siblings)
  20 siblings, 1 reply; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Verify the identity a transparent dispatch constructs, from both
activation paths.

- binfmt_misc_transparent: registers a magic entry with the static 'T'
  flag and execs a matched binary with arguments.

- binfmt_misc_bpf: a handler whose load program sets
  BPF_BINPRM_TRANSPARENT.

Both dispatch to a shared asserting interpreter that runs in place of
the binary and checks the contract from the inside:

- AT_FLAGS carries AT_FLAGS_TRANSPARENT_INTERP
- AT_EXECFD refers to the very inode of the binary
- /proc/self/exe resolves to the binary
- argv and /proc/self/cmdline are exactly what the caller passed with
  nothing spliced in
- comm is the binary's basename
- the binary is write-denied while it runs

The static test also validates the registration. 'T' combined with 'P'
must be rejected. A kernel that does not know 'T' turns the test into a
skip. The asserting interpreter and the static test build without the
bpf toolchain so the core transparent semantics stay covered on systems
where the bpf cases are skipped.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/exec/.gitignore            |   2 +
 tools/testing/selftests/exec/Makefile              |  10 +-
 tools/testing/selftests/exec/binfmt_misc_bpf.c     |  47 +++++++-
 .../selftests/exec/binfmt_misc_transparent.c       | 108 +++++++++++++++++
 .../selftests/exec/binfmt_transparent_interp.c     | 130 +++++++++++++++++++++
 tools/testing/selftests/exec/transparent.bpf.c     |  57 +++++++++
 6 files changed, 349 insertions(+), 5 deletions(-)

diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore
index 8b93b405c424..94b9ab4eb46c 100644
--- a/tools/testing/selftests/exec/.gitignore
+++ b/tools/testing/selftests/exec/.gitignore
@@ -22,5 +22,7 @@ S_I*.test
 binfmt_misc_bpf
 binfmt_bpf_interp
 binfmt_bpf_app
+binfmt_misc_transparent
+binfmt_transparent_interp
 *.bpf.o
 vmlinux.h
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index fb5ca59af51d..6c2fdb674695 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -21,6 +21,11 @@ TEST_GEN_PROGS += recursion-depth
 TEST_GEN_PROGS += null-argv
 TEST_GEN_PROGS += check-exec
 
+# Static ('T' flag) transparent binfmt_misc test; the asserting interpreter
+# is shared with the bpf harness's transparent case. No bpf toolchain needed.
+TEST_GEN_PROGS += binfmt_misc_transparent
+TEST_GEN_FILES += binfmt_transparent_interp
+
 # binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its
 # struct_ops objects and the test interpreter/app it routes between. Only
 # built when clang, bpftool, the vmlinux BTF and libbpf are all present
@@ -35,7 +40,7 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
 			pkg-config --exists libbpf 2>/dev/null && echo y)
 ifeq ($(HAVE_BPF_TOOLCHAIN),y)
 TEST_GEN_PROGS += binfmt_misc_bpf
-TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o
+TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
 TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
 else
 $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
@@ -99,6 +104,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
 $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
 	$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
 
+$(OUTPUT)/binfmt_transparent_interp: binfmt_transparent_interp.c
+	$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
 # PT_INTERP is set to the literal "$ORIGIN/binfmt_bpf_interp"; the nix_origin
 # handler resolves it relative to the binary at run time.
 $(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index 7954c6274623..483c46e13410 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -9,7 +9,7 @@
  *
  *     echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
  *
- * Two self-contained cases are exercised:
+ * Three self-contained cases are exercised:
  *
  *   1. bpf_interp: the match program matches a synthetic aarch64 ELF header
  *      from the prefetched bprm->buf and the load program routes it to a
@@ -18,9 +18,13 @@
  *      commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program
  *      resolves it to an interpreter co-located with the binary (the
  *      relocatable-loader case the kernel ELF loader cannot express).
+ *   3. transparent: the load program sets BPF_BINPRM_TRANSPARENT; the
+ *      asserting interpreter (binfmt_transparent_interp) verifies the
+ *      identity the kernel constructed (exe link, argv, cmdline, comm,
+ *      AT_EXECFD, write denial) from inside the process.
  *
- * Both route to a test interpreter that prints BPF_INTERP_RAN, proving the
- * program's chosen interpreter actually ran.
+ * The first two route to a test interpreter that prints BPF_INTERP_RAN,
+ * proving the program's chosen interpreter actually ran.
  */
 #define _GNU_SOURCE
 #include <elf.h>
@@ -36,12 +40,15 @@
 #include <bpf/libbpf.h>
 
 #include "binfmt_misc_common.h"
-#include "../kselftest_harness.h"
+#include "kselftest_harness.h"
 
 #define INTERP_PATH	"/tmp/binfmt_bpf_interp"
 #define AARCH64_PATH	"/tmp/binfmt_bpf_aarch64"
 #define RELOC_DIR	"/tmp/binfmt_reloc"
+#define TRANS_INTERP	"/tmp/binfmt_transparent_interp"
+#define TRANS_PATH	"/tmp/binfmt_bpf_riscv"
 #define EXPECT		"BPF_INTERP_RAN"
+#define TRANS_EXPECT	"TRANSPARENT_OK"
 
 /* A minimal 64-bit little-endian ELF header, padded to the read size. */
 static int create_fake_elf(const char *path, unsigned short machine)
@@ -77,6 +84,15 @@ static int register_entry(const char *name, const char *handler)
 	return write_reg(rule);
 }
 
+/* Probe for transparent-mode support via its static counterpart. */
+static bool have_transparent(void)
+{
+	if (write_reg(":test_t_probe:E::bmtprobe::/bin/true:T"))
+		return false;
+	unregister("test_t_probe");
+	return true;
+}
+
 static int check_output(const char *cmd, const char *expected)
 {
 	char buf[128];
@@ -211,4 +227,27 @@ TEST_F(bpf_handler, origin_relative_interpreter)
 	rmdir(RELOC_DIR);
 }
 
+/* A transparent dispatch: the process presents as the binary, not the interp. */
+TEST_F(bpf_handler, transparent_dispatch)
+{
+	char src[PATH_MAX], cmd[PATH_MAX + 16];
+
+	if (!have_transparent())
+		SKIP(return, "kernel without transparent mode");
+
+	ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0);
+	ASSERT_EQ(copy_file(src, TRANS_INTERP), 0);
+	ASSERT_EQ(create_fake_elf(TRANS_PATH, EM_RISCV), 0);
+
+	setenv("BINFMT_TEST_BINARY", TRANS_PATH, 1);
+	snprintf(cmd, sizeof(cmd), "%s argone argtwo", TRANS_PATH);
+	ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+				"transparent.bpf.o"), 0);
+	EXPECT_EQ(run_case(self->obj, "transparent", "test_bpf_transparent",
+			   cmd, TRANS_EXPECT), 0);
+
+	unlink(TRANS_PATH);
+	unlink(TRANS_INTERP);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c
new file mode 100644
index 000000000000..de59dc8c5695
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the static transparent flag 'T' of binfmt_misc. A magic-matched
+ * binary is dispatched to an interpreter with the argument vector left
+ * untouched, the binary passed through AT_EXECFD and mm->exe_file labeled
+ * with the binary. The asserting interpreter (binfmt_transparent_interp)
+ * verifies the constructed identity from inside the process and exits 0.
+ *
+ * Needs root for the registration; no bpf toolchain involved.
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define MAGIC		"#TRANSPARENT-SELFTEST#"
+#define TARGET_PATH	"/tmp/binfmt_transparent_target"
+#define INTERP_PATH	"/tmp/binfmt_transparent_interp"
+#define ENTRY		"test_transparent"
+#define TRANSPARENT_ARGV0 "transparent-argv0"
+#define RULE(flags)	":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags
+
+/* The target only has to carry the magic; it is never actually loaded. */
+static int create_target(void)
+{
+	char buf[128] = MAGIC "\n";
+	int fd;
+
+	fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0755);
+	if (fd < 0)
+		return -1;
+	if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
+FIXTURE(transparent) {
+};
+
+FIXTURE_SETUP(transparent)
+{
+	char src[PATH_MAX];
+
+	if (getuid() != 0)
+		SKIP(return, "test must be run as root");
+	if (!binfmt_misc_available())
+		SKIP(return, "no binfmt_misc");
+
+	ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0);
+	ASSERT_EQ(copy_file(src, INTERP_PATH), 0);
+	ASSERT_EQ(create_target(), 0);
+
+	/* Skip the whole suite on a kernel that does not know 'T'. */
+	if (write_reg(RULE("T"))) {
+		ASSERT_EQ(errno, EINVAL);
+		SKIP(return, "kernel without the 'T' flag");
+	}
+	unregister(ENTRY);
+}
+
+FIXTURE_TEARDOWN(transparent)
+{
+	unregister(ENTRY);
+	unlink(TARGET_PATH);
+	unlink(INTERP_PATH);
+}
+
+/* Grammar sanity check: the same entry without 'T' has to register. */
+TEST_F(transparent, plain_entry_registers)
+{
+	ASSERT_EQ(write_reg(RULE("")), 0);
+}
+
+/* 'T' preserves the whole argv, so combining it with 'P' is rejected. */
+TEST_F(transparent, rejects_preserve_argv0)
+{
+	ASSERT_NE(write_reg(RULE("TP")), 0);
+	EXPECT_EQ(errno, EINVAL);
+}
+
+/* The interpreter asserts the identity the kernel built for it. */
+TEST_F(transparent, dispatch)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(write_reg(RULE("T")), 0);
+
+	setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1);
+	setenv("BINFMT_TEST_ARGV0", TRANSPARENT_ARGV0, 1);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		execl(TARGET_PATH, TRANSPARENT_ARGV0, "argone", "argtwo", (char *)NULL);
+		_exit(126);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_transparent_interp.c b/tools/testing/selftests/exec/binfmt_transparent_interp.c
new file mode 100644
index 000000000000..2a71c4706758
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_transparent_interp.c
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Asserting interpreter for the transparent binfmt_misc mode. It runs in
+ * place of the dispatched binary and verifies the identity the kernel
+ * constructed: the aux vector contract, the exe link, argv, cmdline, comm
+ * and the write denial on the binary. BINFMT_TEST_BINARY names the binary;
+ * the harness execs it with the arguments "argone argtwo". Prints
+ * TRANSPARENT_OK and exits 0 when every check holds.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/auxv.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+
+#ifndef AT_FLAGS_TRANSPARENT_INTERP
+#define AT_FLAGS_TRANSPARENT_INTERP (1 << 1)
+#endif
+
+#define TASK_COMM_LEN 16
+
+static int fail;
+
+static void ok(int cond, const char *what)
+{
+	if (!cond) {
+		fprintf(stderr, "TRANSPARENT_FAIL: %s (errno %d)\n", what, errno);
+		fail = 1;
+	}
+}
+
+int main(int argc, char **argv)
+{
+	const char *binary = getenv("BINFMT_TEST_BINARY");
+	const char *argv0 = getenv("BINFMT_TEST_ARGV0");
+	char exe[PATH_MAX], real[PATH_MAX], buf[PATH_MAX];
+	char expect[PATH_MAX + 32];
+	unsigned long execfd;
+	struct stat stb, stfd;
+	const char *want[3];
+	const char *base;
+	size_t expect_len, i;
+	ssize_t n;
+	int fd;
+
+	if (!binary) {
+		fprintf(stderr, "TRANSPARENT_FAIL: BINFMT_TEST_BINARY unset\n");
+		return 1;
+	}
+	/* Distinct from the binary path, so a classic argv splice is caught. */
+	want[0] = argv0 ? argv0 : binary;
+	want[1] = "argone";
+	want[2] = "argtwo";
+
+	/* The aux vector announces the transparent contract. */
+	ok(getauxval(AT_FLAGS) & AT_FLAGS_TRANSPARENT_INTERP,
+	   "AT_FLAGS lacks AT_FLAGS_TRANSPARENT_INTERP");
+
+	/* AT_EXECFD refers to the very file that was executed. */
+	execfd = getauxval(AT_EXECFD);
+	ok(execfd > 2, "no AT_EXECFD");
+	ok(!stat(binary, &stb), "cannot stat the binary");
+	ok(!fstat((int)execfd, &stfd), "cannot fstat AT_EXECFD");
+	ok(stb.st_dev == stfd.st_dev && stb.st_ino == stfd.st_ino,
+	   "AT_EXECFD is not the binary");
+
+	/* The exe link names the binary, not this interpreter. */
+	n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
+	ok(n > 0, "cannot read /proc/self/exe");
+	if (n > 0)
+		exe[n] = '\0';
+	ok(n > 0 && realpath(binary, real) && !strcmp(exe, real),
+	   "/proc/self/exe is not the binary");
+
+	/* argv arrived unspliced. */
+	ok(argc == (int)ARRAY_SIZE(want), "argv was rewritten");
+	for (i = 0; i < ARRAY_SIZE(want) && i < (size_t)argc; i++)
+		ok(!strcmp(argv[i], want[i]), "argv was rewritten");
+
+	/* And so did the kernel's copy of it: the same strings, NUL separated. */
+	for (i = 0, expect_len = 0; i < ARRAY_SIZE(want); i++) {
+		size_t len = strlen(want[i]) + 1;
+
+		memcpy(expect + expect_len, want[i], len);
+		expect_len += len;
+	}
+	fd = open("/proc/self/cmdline", O_RDONLY);
+	n = fd >= 0 ? read(fd, buf, sizeof(buf)) : -1;
+	if (fd >= 0)
+		close(fd);
+	ok(n == (ssize_t)expect_len && !memcmp(buf, expect, expect_len),
+	   "/proc/self/cmdline was rewritten");
+
+	/* comm is the binary's basename. */
+	base = strrchr(binary, '/');
+	base = base ? base + 1 : binary;
+	snprintf(expect, TASK_COMM_LEN, "%s", base);
+	fd = open("/proc/self/comm", O_RDONLY);
+	n = fd >= 0 ? read(fd, buf, sizeof(buf) - 1) : -1;
+	if (fd >= 0)
+		close(fd);
+	if (n > 0 && buf[n - 1] == '\n')
+		n--;
+	if (n >= 0)
+		buf[n] = '\0';
+	ok(n > 0 && !strcmp(buf, expect), "comm is not the binary's basename");
+
+	/* The binary is write-denied while it runs, like a direct exec. */
+	errno = 0;
+	fd = open(binary, O_WRONLY);
+	ok(fd < 0 && errno == ETXTBSY, "binary is writable while running");
+	if (fd >= 0)
+		close(fd);
+	errno = 0;
+	fd = open("/proc/self/exe", O_WRONLY);
+	ok(fd < 0 && errno == ETXTBSY, "exe link is writable while running");
+	if (fd >= 0)
+		close(fd);
+
+	if (!fail)
+		printf("TRANSPARENT_OK\n");
+	return fail;
+}
diff --git a/tools/testing/selftests/exec/transparent.bpf.c b/tools/testing/selftests/exec/transparent.bpf.c
new file mode 100644
index 000000000000..7632019ebe69
--- /dev/null
+++ b/tools/testing/selftests/exec/transparent.bpf.c
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binfmt_misc_ops handler for the transparent-mode case: match a synthetic
+ * riscv ELF header and run the asserting interpreter transparently - the
+ * argument vector untouched, the binary in AT_EXECFD and mm->exe_file
+ * labeled with the binary.
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+#define EI_CLASS	4
+#define ELFCLASS64	2
+#define EM_RISCV	243
+
+extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
+				 size_t path__sz) __ksym;
+extern int bpf_binprm_set_flags(struct linux_binprm *bprm,
+				enum bpf_binprm_flags flags) __ksym;
+
+SEC("struct_ops.s/match")
+bool BPF_PROG(transparent_match, struct linux_binprm *bprm)
+{
+	__u16 machine;
+
+	if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
+	    bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
+	    bprm->buf[EI_CLASS] != ELFCLASS64)
+		return false;
+
+	/* e_machine is a 16-bit little-endian field at offset 18. */
+	machine = (__u8)bprm->buf[18] | ((__u16)(__u8)bprm->buf[19] << 8);
+	return machine == EM_RISCV;
+}
+
+SEC("struct_ops.s/load")
+int BPF_PROG(transparent_load, struct linux_binprm *bprm)
+{
+	char interp[] = "/tmp/binfmt_transparent_interp";
+	int err;
+
+	err = bpf_binprm_set_flags(bprm, BPF_BINPRM_TRANSPARENT);
+	if (err)
+		return err;
+
+	/* @path__sz includes the terminating NUL; 0 commits the selection. */
+	return bpf_binprm_set_interp(bprm, interp, sizeof(interp));
+}
+
+SEC(".struct_ops.link")
+struct binfmt_misc_ops transparent = {
+	.match = (void *)transparent_match,
+	.load = (void *)transparent_load,
+	.name = "transparent",
+};

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 15/21] binfmt_misc: document the transparent identity contract
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (13 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 16/21] exec: carry a PT_INTERP substitute in struct linux_binprm Christian Brauner
                   ` (5 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Describe what a transparent dispatch constructs and the loader contract
behind AT_FLAGS_TRANSPARENT_INTERP. Also note what deliberately stays
different (the address space layout) and what stays unchanged
(credential derivation without 'C').

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index b27ad31847ab..0946ca1923f2 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -218,6 +218,31 @@ binfmt_misc instances themselves are looked up. The entry keeps the handler
 alive; deleting the struct_ops map only prevents new activations.
 
 
+Transparent interpreters
+------------------------
+
+With the ``T`` flag or ``BPF_BINPRM_TRANSPARENT`` the dispatch is invisible
+to the resulting process. The argument vector is left exactly as the caller
+built it. The binary is passed through ``AT_EXECFD``. The kernel also labels
+``/proc/pid/exe`` correctly. The binary's file is write-denied while the
+process runs while the interpreter is not, exactly as if it had been executed
+directly. A transparent entry does not change how credentials are derived. As
+with any other entry, set*id bits of the binary are only honored with ``C`` (or
+``BPF_BINPRM_CREDENTIALS``).
+
+The interpreter has to be built for this contract. The kernel announces it
+with ``AT_FLAGS_TRANSPARENT_INTERP`` in the ``AT_FLAGS`` aux vector entry
+next to ``AT_EXECFD``. The argument vector belongs entirely to the program,
+nothing was spliced in, so the interpreter doesn't consume arguments and
+simply loads the program from the descriptor. The bit is also the loader's
+license to finish the identity. After mapping the program it may retarget the
+``AT_PHDR``/``AT_ENTRY``/``AT_BASE`` entries of ``/proc/pid/auxv`` and the
+code/data statistics markers via one ``PR_SET_MM_MAP`` which completes
+what attaching debuggers observe. What remains visibly different from a direct
+execution is the address space layout. The interpreter occupies the main-image
+position and the program lives in the mmap region.
+
+
 Hints
 -----
 

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 16/21] exec: carry a PT_INTERP substitute in struct linux_binprm
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (14 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 15/21] binfmt_misc: document the transparent identity contract Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 17/21] binfmt_elf: consume a stashed PT_INTERP substitute Christian Brauner
                   ` (4 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

binfmt_misc currently supports an execution model where the registered
interpreter becomes the executed program and the matched binary is
handed to it as payload. The upcoming binfmt_misc loader mode inverts
this. The matched binary remains the executed program and the registered
interpreter is substituted into the role the binary's PT_INTERP would
have played.

Add the channel for that hand-over. bprm->loader carries an
open_exec-style struct file reference from the binfmt_misc match to the
binary format that consumes it. Unlike bprm->interpreter it does not
request a restart of the format search. The stashing handler declines
the exec with -ENOEXEC and the search continues to the real format in
the same round.

Establish the complete lifecycle up front so a stashed loader can
neither leak nor be silently ignored.

- Chain restart: if another format wins the round by staging
  bprm->interpreter (binfmt_script) the stashed loader belonged to
  the file being replaced. Drop it at the top of the swap block in
  exec_binprm().

- Unclaimed or error: free_bprm() releases a still-stashed loader
  next to the other bprm file references.

- Silent non-substitution: a final format that reaches
  begin_new_exec() with a pending loader would run the binary while
  ignoring the override. Refuse with -ENOEXEC before the point of no
  return. Formats that do not know about the override (binfmt_flat,
  binfmt_elf_fdpic, out-of-tree) need no changes.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/exec.c               | 9 +++++++++
 include/linux/binfmts.h | 1 +
 2 files changed, 10 insertions(+)

diff --git a/fs/exec.c b/fs/exec.c
index 45d416994682..797d9a0cf6c6 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1123,6 +1123,10 @@ int begin_new_exec(struct linux_binprm * bprm)
 	struct task_struct *me = current;
 	int retval;
 
+	/* A pending PT_INTERP substitution this format cannot consume. */
+	if (bprm->loader)
+		return -ENOEXEC;
+
 	/* A declined execfd request has no executable for a later format. */
 	if (!bprm->executable) {
 		bprm->have_execfd = 0;
@@ -1435,6 +1439,8 @@ static void free_bprm(struct linux_binprm *bprm)
 	if (bprm->old_mm)
 		exec_mm_put_old(bprm->old_mm);
 	do_close_execat(bprm->file);
+	/* An unconsumed PT_INTERP substitute from a binfmt_misc loader entry. */
+	do_close_execat(bprm->loader);
 	if (bprm->executable) {
 		/* A transparent dispatch still holds the write denial. */
 		if (bprm->executable_denied)
@@ -1757,6 +1763,9 @@ static int exec_binprm(struct linux_binprm *bprm)
 		if (!bprm->interpreter)
 			break;
 
+		/* A stashed PT_INTERP substitute belonged to the replaced file. */
+		do_close_execat(no_free_ptr(bprm->loader));
+
 		exec = bprm->file;
 		bprm->file = bprm->interpreter;
 		bprm->interpreter = NULL;
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index ba5037b69866..aa6dac777173 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -64,6 +64,7 @@ struct linux_binprm {
 		executable_denied:1;
 	struct file *executable; /* Executable to pass to the interpreter */
 	struct file *interpreter;
+	struct file *loader;
 	struct file *file;
 	struct cred *cred;	/* new credentials */
 	int unsafe;		/* how unsafe this exec is (mask of LSM_UNSAFE_*) */

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 17/21] binfmt_elf: consume a stashed PT_INTERP substitute
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (15 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 16/21] exec: carry a PT_INTERP substitute in struct linux_binprm Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 18/21] binfmt_misc: add the 'L' loader substitution flag Christian Brauner
                   ` (3 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

When a binfmt_misc loader entry stashed bprm->loader use it instead of
opening the path named in PT_INTERP. The substitution deliberately
changes as little as possible. Ownership transfers into the local
interpreter reference which the existing success and error paths already
release.

A binary without PT_INTERP has nothing to substitute for. Drop the
override at the end of the segment scan and load the binary natively.

Nothing sets bprm->loader yet.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_elf.c | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index e44230242f3a..3455af6be2b5 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -19,6 +19,7 @@
 #include <linux/errno.h>
 #include <linux/signal.h>
 #include <linux/binfmts.h>
+#include <linux/cleanup.h>
 #include <linux/string.h>
 #include <linux/file.h>
 #include <linux/slab.h>
@@ -907,7 +908,11 @@ static int load_elf_binary(struct linux_binprm *bprm)
 		if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
 			goto out_free_interp;
 
-		interpreter = open_exec(elf_interpreter);
+		/* A binfmt_misc loader entry substitutes for PT_INTERP. */
+		if (bprm->loader)
+			interpreter = no_free_ptr(bprm->loader);
+		else
+			interpreter = open_exec(elf_interpreter);
 		kfree(elf_interpreter);
 		retval = PTR_ERR(interpreter);
 		if (IS_ERR(interpreter))
@@ -938,6 +943,14 @@ static int load_elf_binary(struct linux_binprm *bprm)
 		goto out_free_ph;
 	}
 
+	/* No PT_INTERP to substitute for: the override does not apply. */
+	if (bprm->loader) {
+		struct file *loader = no_free_ptr(bprm->loader);
+
+		exe_file_allow_write_access(loader);
+		fput(loader);
+	}
+
 	elf_ppnt = elf_phdata;
 	for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
 		switch (elf_ppnt->p_type) {

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 18/21] binfmt_misc: add the 'L' loader substitution flag
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (16 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 17/21] binfmt_elf: consume a stashed PT_INTERP substitute Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 19/21] binfmt_misc: let a bpf handler request loader substitution Christian Brauner
                   ` (2 subsequent siblings)
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Add the first activation of the PT_INTERP substitution machinery. A
static entry registered with the new 'L' flag no longer runs the
registered interpreter with the binary as payload. It stashes the
interpreter as bprm->loader and declines the match with -ENOEXEC. The
format search continues in the same round. binfmt_elf claims the binary
as a fully native exec and substitutes the stashed file for the binary's
PT_INTERP.

'L' rejects every classic-dispatch flag at registration. 'T', 'P' and
'O' have nothing to act on (no argv splice, no execfd) and 'C' is
subsumed (credentials derive from the binary natively).
'F' composes and is valuable. The substitute is pre-opened at
registration time and immune to mount namespace changes. As with 'C',
only trusted interpreters should be registered. The substituted
loader runs with credentials derived from the binary.

The interpreter open is shared with the classic path via the new
entry_open_interpreter() helper. Open errors fail the exec as they do
for classic entries. Like the other flag characters 'L' cannot be
used as the field delimiter or the flag scan would run off the
registration buffer.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/binfmt_misc.c | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 1cd30dec3fab..ee48dab2638a 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -51,6 +51,7 @@ enum binfmt_misc_entry_flags {
 	MISC_FMT_CREDENTIALS	= (1U << 29),
 	MISC_FMT_OPEN_FILE	= (1U << 28),
 	MISC_FMT_TRANSPARENT	= (1U << 27),
+	MISC_FMT_LOADER		= (1U << 26),
 };
 
 /**
@@ -73,6 +74,7 @@ static const struct binfmt_misc_flag misc_flags[] = {
 	{ 'C', MISC_FMT_CREDENTIALS,	MISC_FMT_OPEN_BINARY,	"credentials from the binary"	},
 	{ 'F', MISC_FMT_OPEN_FILE,	0,			"open interpreter file now"	},
 	{ 'T', MISC_FMT_TRANSPARENT,	MISC_FMT_OPEN_BINARY,	"transparent"			},
+	{ 'L', MISC_FMT_LOADER,		0,			"loader substitution"		},
 };
 
 /* Look up a flag character, NULL if @c is not one. */
@@ -475,6 +477,24 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (flags & MISC_FMT_OPEN_BINARY)
 		bprm->have_execfd = 1;
 
+	/*
+	 * Stash the interpreter for binfmt_elf to consume in place of the
+	 * binary's PT_INTERP and decline the match, so the search continues
+	 * to the real format in the same round.
+	 */
+	if (flags & MISC_FMT_LOADER) {
+		/* A native exec has no argv slot for a staged argument. */
+		if (bprm->bpf_interp_arg)
+			return -EINVAL;
+
+		interp_file = entry_open_interpreter(fmt, interpreter);
+		if (IS_ERR(interp_file))
+			return PTR_ERR(interp_file);
+
+		bprm->loader = interp_file;
+		return -ENOEXEC;
+	}
+
 	if (flags & MISC_FMT_TRANSPARENT) {
 		/* No argv is built for a staged argument to land in. */
 		kfree(bprm->bpf_interp_arg);
@@ -771,6 +791,12 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 	    (e->flags & MISC_FMT_PRESERVE_ARGV0))
 		return ERR_PTR(-EINVAL);
 
+	/* A native exec splices no argv, passes no execfd and needs no creds. */
+	if ((e->flags & MISC_FMT_LOADER) &&
+	    (e->flags & (MISC_FMT_TRANSPARENT | MISC_FMT_PRESERVE_ARGV0 |
+			 MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY)))
+		return ERR_PTR(-EINVAL);
+
 	if (*p == '\n')
 		p++;
 	if (p != buf + count)

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 19/21] binfmt_misc: let a bpf handler request loader substitution
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (17 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 18/21] binfmt_misc: add the 'L' loader substitution flag Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20  9:33 ` [PATCH 20/21] selftests/exec: test binfmt_misc " Christian Brauner
  2026-07-20  9:33 ` [PATCH 21/21] binfmt_misc: document " Christian Brauner
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Give bpf handlers the per-exec equivalent of the static 'L' flag. A
load program that sets BPF_BINPRM_LOADER has its selected interpreter
substituted for the binary's PT_INTERP instead of run with the binary
as payload. The binary otherwise executes as a fully native exec.

A single handler can now grade its dispatch per binary: native-arch ELF
with PT_INTERP gets loader substitution for full native identity.
Anything else, such as foreign arch, static, non-ELF can use transparent
or classic dispatch. The load program can read the binary's ELF header
from bprm->buf to make that call.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst |  3 +++
 fs/binfmt_misc.c                          |  2 ++
 fs/binfmt_misc_bpf.c                      | 13 ++++++++++---
 include/linux/binfmt_misc.h               |  4 ++++
 4 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 0946ca1923f2..8c861bf0c56c 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -208,6 +208,9 @@ decide them differently for each binary it handles:
   run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor to
   ``execveat()``, which a path-splicing dispatch cannot: the interpreter
   has no path by which to open it.
+- ``BPF_BINPRM_LOADER`` substitutes the interpreter for the binary's
+  ``PT_INTERP`` and runs the binary as a fully native exec (the ``L``
+  flag). It excludes the other flags and a staged interpreter argument.
 
 Because these are program choices, a ``B`` entry carries no flags in the
 register string; ``F`` (pre-open a fixed interpreter) has no meaning for it.
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index ee48dab2638a..855b41016cf0 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -354,6 +354,8 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
 		flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY;
 	if (bpf_flags & BPF_BINPRM_TRANSPARENT)
 		flags |= MISC_FMT_TRANSPARENT | MISC_FMT_OPEN_BINARY;
+	if (bpf_flags & BPF_BINPRM_LOADER)
+		flags |= MISC_FMT_LOADER;
 
 	return flags;
 }
diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c
index d06cacc5c8c7..105c8afbb3f2 100644
--- a/fs/binfmt_misc_bpf.c
+++ b/fs/binfmt_misc_bpf.c
@@ -179,8 +179,10 @@ __bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
  * BPF_BINPRM_CREDENTIALS computes credentials from the binary, and
  * BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD.
  * BPF_BINPRM_TRANSPARENT additionally leaves the argument vector untouched,
- * making the exec look like a direct execution of the binary. Calling it
- * again replaces the flags, passing zero clears them again.
+ * making the exec look like a direct execution of the binary.
+ * BPF_BINPRM_LOADER substitutes the interpreter for the binary's PT_INTERP
+ * and runs the binary as a native exec; it excludes every other flag.
+ * Calling it again replaces the flags, passing zero clears them again.
  *
  * Return: 0 on success, -EINVAL if @flags contains an unknown bit or an
  * invalid combination
@@ -189,7 +191,12 @@ __bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm,
 				     enum bpf_binprm_flags flags)
 {
 	if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS |
-		      BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT))
+		      BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT |
+		      BPF_BINPRM_LOADER))
+		return -EINVAL;
+
+	/* Loader substitution is a native exec: no splice, execfd or creds work. */
+	if ((flags & BPF_BINPRM_LOADER) && (flags & ~BPF_BINPRM_LOADER))
 		return -EINVAL;
 
 	/* Transparency preserves the whole argv, argv[0] included. */
diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h
index 26da749391b4..4abdfd36b3fa 100644
--- a/include/linux/binfmt_misc.h
+++ b/include/linux/binfmt_misc.h
@@ -19,6 +19,9 @@ struct user_namespace;
  * @BPF_BINPRM_TRANSPARENT: leave argv untouched, the interpreter takes the
  *                          binary from AT_EXECFD (like the 'T' flag); implies
  *                          execfd, excludes preserve-argv0
+ * @BPF_BINPRM_LOADER: substitute the interpreter for the binary's PT_INTERP
+ *                     and run the binary as a native exec (like the 'L'
+ *                     flag); excludes every other flag
  *
  * Set from a load program with bpf_binprm_set_flags(). Unlike a static entry,
  * a bpf handler chooses these per exec rather than once at registration.
@@ -28,6 +31,7 @@ enum bpf_binprm_flags {
 	BPF_BINPRM_CREDENTIALS		= (1ULL << 1),
 	BPF_BINPRM_EXECFD		= (1ULL << 2),
 	BPF_BINPRM_TRANSPARENT		= (1ULL << 3),
+	BPF_BINPRM_LOADER		= (1ULL << 4),
 };
 
 /**

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 20/21] selftests/exec: test binfmt_misc loader substitution
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (18 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 19/21] binfmt_misc: let a bpf handler request loader substitution Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  2026-07-20 12:22   ` sashiko-bot
  2026-07-20  9:33 ` [PATCH 21/21] binfmt_misc: document " Christian Brauner
  20 siblings, 1 reply; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Exercise the 'L' flag end to end. The payload runs as the main image
with a copy of the system loader substituted for its PT_INTERP, and
asserts the native identity from inside:

- argv exactly as the caller built it
- no AT_EXECFD
- AT_FLAGS clear
- AT_BASE set but outside its own image
- AT_PHDR/AT_ENTRY inside it
- /proc/self/{exe,comm,stat} and AT_EXECFN all describing the binary
- ETXTBSY on the running binary
- the substituted loader visible in /proc/self/maps under its real path

Magic matching pokes a marker into the ELF header's e_ident padding
(EI_PAD, offset 9), which sits inside the match window and is ignored by
kernel and loader alike. the same binary is also matched by extension.

The bpf-side BPF_BINPRM_LOADER path shares all machinery past the flag
mapping. A harness case for it can join the bpf runtime coverage of
the transparent series.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/exec/.gitignore            |   3 +
 tools/testing/selftests/exec/Makefile              |  14 +
 .../testing/selftests/exec/binfmt_loader_payload.c | 196 +++++++++++
 tools/testing/selftests/exec/binfmt_misc_bpf.c     | 132 ++++++--
 tools/testing/selftests/exec/binfmt_misc_common.h  |  48 +++
 tools/testing/selftests/exec/binfmt_misc_loader.c  | 363 +++++++++++++++++++++
 tools/testing/selftests/exec/loader.bpf.c          |  56 ++++
 7 files changed, 786 insertions(+), 26 deletions(-)

diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore
index 94b9ab4eb46c..fbbb1600ddb9 100644
--- a/tools/testing/selftests/exec/.gitignore
+++ b/tools/testing/selftests/exec/.gitignore
@@ -24,5 +24,8 @@ binfmt_bpf_interp
 binfmt_bpf_app
 binfmt_misc_transparent
 binfmt_transparent_interp
+binfmt_misc_loader
+binfmt_loader_payload
+binfmt_loader_payload_static
 *.bpf.o
 vmlinux.h
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 6c2fdb674695..de3f0a61b999 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -26,6 +26,13 @@ TEST_GEN_PROGS += check-exec
 TEST_GEN_PROGS += binfmt_misc_transparent
 TEST_GEN_FILES += binfmt_transparent_interp
 
+# 'L' (loader substitution) binfmt_misc test: the payload runs as the main
+# image with a copy of the system loader substituted for its PT_INTERP and
+# asserts the native identity from inside; the static build proves the
+# override is dropped for a binary without PT_INTERP.
+TEST_GEN_PROGS += binfmt_misc_loader
+TEST_GEN_FILES += binfmt_loader_payload binfmt_loader_payload_static
+
 # binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its
 # struct_ops objects and the test interpreter/app it routes between. Only
 # built when clang, bpftool, the vmlinux BTF and libbpf are all present
@@ -41,6 +48,7 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
 ifeq ($(HAVE_BPF_TOOLCHAIN),y)
 TEST_GEN_PROGS += binfmt_misc_bpf
 TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
+TEST_GEN_FILES += loader.bpf.o
 TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
 else
 $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
@@ -107,6 +115,12 @@ $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
 $(OUTPUT)/binfmt_transparent_interp: binfmt_transparent_interp.c
 	$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
 
+$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c
+	$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
+
+$(OUTPUT)/binfmt_loader_payload_static: binfmt_loader_payload.c
+	$(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@
+
 # PT_INTERP is set to the literal "$ORIGIN/binfmt_bpf_interp"; the nix_origin
 # handler resolves it relative to the binary at run time.
 $(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c
diff --git a/tools/testing/selftests/exec/binfmt_loader_payload.c b/tools/testing/selftests/exec/binfmt_loader_payload.c
new file mode 100644
index 000000000000..690fd8d6531a
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_loader_payload.c
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Payload for the binfmt_misc 'L' (loader substitution) selftest. It is
+ * executed as the MAIN image - a fully native exec - with the registered
+ * interpreter substituted for its PT_INTERP, and asserts the native
+ * identity from the inside. Exits 0 when every surface checks out.
+ *
+ * Modes, selected by the orchestrator via the environment:
+ *  - default:                full assertions, path-based ones included
+ *  - BINFMT_TEST_MEMFD=1:    executed from an inaccessible memfd, skip
+ *                            the path-based assertions
+ *  - BINFMT_TEST_STATIC=1:   static build; the override was dropped, so
+ *                            expect no interpreter at all
+ */
+#define _GNU_SOURCE
+#include <elf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/auxv.h>
+#include <unistd.h>
+
+/* Start of our own mapped image, courtesy of the linker. */
+extern const char __ehdr_start[];
+
+/* An image is never this large; used to bracket "within our image". */
+#define IMAGE_SPAN (16UL << 20)
+
+static int failed;
+
+static void check(int cond, const char *what)
+{
+	if (cond)
+		return;
+	fprintf(stderr, "[payload] FAILED: %s (errno %d)\n", what, errno);
+	failed = 1;
+}
+
+/* start_code and end_code are the 26th and 27th fields of /proc/pid/stat. */
+static int stat_codes(unsigned long *start_code, unsigned long *end_code)
+{
+	char buf[4096], *p;
+	ssize_t n;
+	int fd, i;
+
+	fd = open("/proc/self/stat", O_RDONLY);
+	if (fd < 0)
+		return -1;
+	n = read(fd, buf, sizeof(buf) - 1);
+	close(fd);
+	if (n <= 0)
+		return -1;
+	buf[n] = '\0';
+
+	/* Skip "pid (comm)", then start_code is the 24th field after it. */
+	p = strrchr(buf, ')');
+	if (!p)
+		return -1;
+	p++;
+	for (i = 0; i < 23; i++) {
+		p = strchr(p + 1, ' ');
+		if (!p)
+			return -1;
+	}
+	if (sscanf(p, " %lu %lu", start_code, end_code) != 2)
+		return -1;
+	return 0;
+}
+
+/* Return whether /proc/self/maps names a path starting with @prefix. */
+static int maps_has_prefix(const char *prefix)
+{
+	char *line = NULL;
+	size_t len = 0;
+	int found = 0;
+	FILE *f;
+
+	f = fopen("/proc/self/maps", "r");
+	if (!f)
+		return -1;
+	while (getline(&line, &len, f) > 0) {
+		char *path = strchr(line, '/');
+
+		if (path && !strncmp(path, prefix, strlen(prefix))) {
+			found = 1;
+			break;
+		}
+	}
+	free(line);
+	fclose(f);
+	return found;
+}
+
+int main(int argc, char *argv[])
+{
+	const char *binary = getenv("BINFMT_TEST_BINARY");
+	const char *interp = getenv("BINFMT_TEST_INTERP");
+	int memfd_mode = getenv("BINFMT_TEST_MEMFD") != NULL;
+	int static_mode = getenv("BINFMT_TEST_STATIC") != NULL;
+	unsigned long self = (unsigned long)__ehdr_start;
+	unsigned long base = getauxval(AT_BASE);
+	unsigned long phdr = getauxval(AT_PHDR);
+	unsigned long entry = getauxval(AT_ENTRY);
+	unsigned long start_code, end_code;
+	char buf[PATH_MAX];
+	ssize_t n;
+
+	/* The argument vector is exactly what the caller built. */
+	check(argc == 3 && !strcmp(argv[0], "payload-argv0") &&
+	      !strcmp(argv[1], "argone") && !strcmp(argv[2], "argtwo"),
+	      "argv was rewritten");
+
+	/* Native from birth: no execfd, no dispatch marker. */
+	check(getauxval(AT_EXECFD) == 0, "AT_EXECFD present");
+	check(getauxval(AT_FLAGS) == 0, "AT_FLAGS not native");
+
+	if (static_mode) {
+		/* The override was dropped: no interpreter was loaded. */
+		check(base == 0, "AT_BASE set for a static payload");
+	} else {
+		/* A loader is mapped in the interpreter slot, not our image. */
+		check(base != 0, "AT_BASE missing");
+		check(base < self || base >= self + IMAGE_SPAN,
+		      "AT_BASE inside our own image");
+	}
+
+	/* We occupy the main-image slot. */
+	check(phdr >= self && phdr < self + IMAGE_SPAN,
+	      "AT_PHDR outside our image");
+	check(entry >= self && entry < self + IMAGE_SPAN,
+	      "AT_ENTRY outside our image");
+
+	/* The code statistics markers describe our image, natively placed. */
+	if (stat_codes(&start_code, &end_code) == 0) {
+		check(start_code >= self && start_code < end_code &&
+		      end_code < self + IMAGE_SPAN,
+		      "stat start_code/end_code not our image");
+		check(entry >= start_code && entry < end_code,
+		      "AT_ENTRY outside [start_code, end_code)");
+	} else {
+		check(0, "cannot parse /proc/self/stat");
+	}
+
+	if (!memfd_mode && binary) {
+		const char *execfn = (const char *)getauxval(AT_EXECFN);
+		const char *base_name = strrchr(binary, '/');
+		char comm[32] = "";
+		int fd;
+
+		base_name = base_name ? base_name + 1 : binary;
+
+		/* exe link, AT_EXECFN and comm all follow the binary. */
+		n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
+		check(n > 0, "readlink /proc/self/exe");
+		if (n > 0) {
+			buf[n] = '\0';
+			check(!strcmp(buf, binary), "/proc/self/exe");
+		}
+
+		check(execfn && !strcmp(execfn, binary), "AT_EXECFN");
+
+		fd = open("/proc/self/comm", O_RDONLY);
+		check(fd >= 0, "open /proc/self/comm");
+		if (fd >= 0) {
+			n = read(fd, comm, sizeof(comm) - 1);
+			if (n > 0 && comm[n - 1] == '\n')
+				n--;
+			comm[n > 0 ? n : 0] = '\0';
+			check(!strncmp(comm, base_name, 15), "comm");
+			close(fd);
+		}
+
+		/* The running binary is write-denied, natively. */
+		check(open(binary, O_WRONLY) < 0 && errno == ETXTBSY,
+		      "no ETXTBSY on the binary");
+	}
+
+	if (interp) {
+		int found = maps_has_prefix(interp);
+
+		if (static_mode)
+			/* Nothing was substituted, nothing may be mapped. */
+			check(found == 0, "loader mapped for a static payload");
+		else
+			/* The substituted loader shows under its real path. */
+			check(found == 1, "loader path not in /proc/self/maps");
+	}
+
+	if (failed)
+		return 1;
+	printf("[payload] native identity checks out\n");
+	return 0;
+}
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index 483c46e13410..926db50d39d5 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -22,6 +22,10 @@
  *      asserting interpreter (binfmt_transparent_interp) verifies the
  *      identity the kernel constructed (exe link, argv, cmdline, comm,
  *      AT_EXECFD, write denial) from inside the process.
+ *   4. loader: the load program sets BPF_BINPRM_LOADER; the payload
+ *      (binfmt_loader_payload) runs as the main image with the selected
+ *      interpreter substituted for its PT_INTERP and asserts the native
+ *      identity from inside.
  *
  * The first two route to a test interpreter that prints BPF_INTERP_RAN,
  * proving the program's chosen interpreter actually ran.
@@ -35,6 +39,7 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <sys/stat.h>
+#include <sys/wait.h>
 
 #include <bpf/btf.h>
 #include <bpf/libbpf.h>
@@ -49,6 +54,9 @@
 #define TRANS_PATH	"/tmp/binfmt_bpf_riscv"
 #define EXPECT		"BPF_INTERP_RAN"
 #define TRANS_EXPECT	"TRANSPARENT_OK"
+#define LOADER_INTERP	"/tmp/binfmt_loader_interp"
+#define LOADER_PATH	"/tmp/binfmt_bpf_loader.ldrtest"
+#define LOADER_MARKER	"LDRTST"
 
 /* A minimal 64-bit little-endian ELF header, padded to the read size. */
 static int create_fake_elf(const char *path, unsigned short machine)
@@ -109,52 +117,93 @@ static int check_output(const char *cmd, const char *expected)
 	return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
 }
 
+/* An attached handler with its 'B' entry activated. */
+struct bpf_case {
+	struct bpf_object *obj;
+	struct bpf_link *link;
+	const char *entry;
+};
+
 /*
  * Load @objfile, attach its struct_ops map @handler (which publishes the
- * handler), activate a 'B' entry named @entry that references it, run @target
- * and check it produced @expect.
+ * handler) and activate a 'B' entry named @entry that references it.
  */
-static int run_case(const char *objfile, const char *handler,
-		    const char *entry, const char *target, const char *expect)
+static int bpf_case_start(struct bpf_case *c, const char *objfile,
+			  const char *handler, const char *entry)
 {
-	struct bpf_object *obj;
-	struct bpf_map *map;
-	struct bpf_link *link;
-	int ret = -1;
+	c->obj = NULL;
+	c->link = NULL;
+	c->entry = entry;
 
-	obj = bpf_object__open_file(objfile, NULL);
-	if (!obj || libbpf_get_error(obj)) {
+	c->obj = bpf_object__open_file(objfile, NULL);
+	if (!c->obj || libbpf_get_error(c->obj)) {
 		fprintf(stderr, "open %s failed\n", objfile);
+		c->obj = NULL;
 		return -1;
 	}
-	if (bpf_object__load(obj)) {
+	if (bpf_object__load(c->obj)) {
 		fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n",
 			objfile);
-		goto close;
-	}
-	map = bpf_object__find_map_by_name(obj, handler);
-	if (!map) {
-		fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile);
-		goto close;
+		goto fail;
 	}
-	link = bpf_map__attach_struct_ops(map);
-	if (!link || libbpf_get_error(link)) {
+	c->link = bpf_map__attach_struct_ops(
+			bpf_object__find_map_by_name(c->obj, handler));
+	if (!c->link || libbpf_get_error(c->link)) {
 		fprintf(stderr, "attach struct_ops '%s' failed\n", handler);
-		goto close;
+		c->link = NULL;
+		goto fail;
 	}
 	if (register_entry(entry, handler)) {
 		fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
-		goto detach;
+		goto fail;
 	}
+	return 0;
+
+fail:
+	bpf_link__destroy(c->link);
+	bpf_object__close(c->obj);
+	c->obj = NULL;
+	c->link = NULL;
+	return -1;
+}
+
+static void bpf_case_stop(struct bpf_case *c)
+{
+	unregister(c->entry);
+	bpf_link__destroy(c->link);
+	bpf_object__close(c->obj);
+}
+
+/* Activate @handler, run @target and check it produced @expect. */
+static int run_case(const char *objfile, const char *handler,
+		    const char *entry, const char *target, const char *expect)
+{
+	struct bpf_case c;
+	int ret;
+
+	if (bpf_case_start(&c, objfile, handler, entry))
+		return -1;
 	ret = check_output(target, expect);
-	unregister(entry);
-detach:
-	bpf_link__destroy(link);
-close:
-	bpf_object__close(obj);
+	bpf_case_stop(&c);
 	return ret;
 }
 
+/* Run @path with the argv binfmt_loader_payload asserts, return its status. */
+static int run_payload(const char *path)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	if (pid == 0) {
+		execl(path, "payload-argv0", "argone", "argtwo", (char *)NULL);
+		_exit(126);
+	}
+	if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+		return -1;
+	return WEXITSTATUS(status);
+}
+
 FIXTURE(bpf_handler) {
 	char obj[PATH_MAX];	/* struct_ops object of the case under test */
 };
@@ -250,4 +299,35 @@ TEST_F(bpf_handler, transparent_dispatch)
 	unlink(TRANS_INTERP);
 }
 
+/* A per-exec loader substitution: the payload runs as a native exec. */
+TEST_F(bpf_handler, loader_substitution)
+{
+	char src[PATH_MAX], loader[PATH_MAX];
+	struct bpf_case c;
+	int status;
+
+	if (find_loader(loader, sizeof(loader)))
+		SKIP(return, "cannot determine own PT_INTERP");
+
+	ASSERT_EQ(copy_file(loader, LOADER_INTERP), 0);
+	ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0);
+	ASSERT_EQ(copy_file(src, LOADER_PATH), 0);
+	ASSERT_EQ(patch_file(LOADER_PATH, EI_PAD, LOADER_MARKER,
+			     strlen(LOADER_MARKER)), 0);
+	ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+				"loader.bpf.o"), 0);
+
+	setenv("BINFMT_TEST_BINARY", LOADER_PATH, 1);
+	setenv("BINFMT_TEST_INTERP", LOADER_INTERP, 1);
+
+	ASSERT_EQ(bpf_case_start(&c, self->obj, "loader", "test_bpf_loader"), 0);
+	status = run_payload(LOADER_PATH);
+	bpf_case_stop(&c);
+	EXPECT_EQ(status, 0);
+
+	unsetenv("BINFMT_TEST_INTERP");
+	unlink(LOADER_PATH);
+	unlink(LOADER_INTERP);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
index 854c7a6d34e1..b92370953125 100644
--- a/tools/testing/selftests/exec/binfmt_misc_common.h
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -3,14 +3,17 @@
 #ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
 #define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
 
+#include <elf.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <libgen.h>
 #include <limits.h>
+#include <link.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <string.h>
 #include <sys/mount.h>
+#include <sys/types.h>
 #include <unistd.h>
 
 #define BINFMT_DIR	"/proc/sys/fs/binfmt_misc"
@@ -95,4 +98,49 @@ static inline int artifact_path(char *out, size_t sz, const char *name)
 	return 0;
 }
 
+static inline int patch_file(const char *path, off_t off, const void *data, size_t len)
+{
+	ssize_t n;
+	int fd;
+
+	fd = open(path, O_WRONLY);
+	if (fd < 0)
+		return -1;
+	n = pwrite(fd, data, len, off);
+	close(fd);
+	return n == (ssize_t)len ? 0 : -1;
+}
+
+/* Find the system loader through our own PT_INTERP. */
+static inline int find_loader(char *out, size_t sz)
+{
+	ElfW(Ehdr) eh;
+	ElfW(Phdr) ph;
+	int fd, i, ret = -1;
+
+	fd = open("/proc/self/exe", O_RDONLY);
+	if (fd < 0)
+		return -1;
+	if (pread(fd, &eh, sizeof(eh), 0) != sizeof(eh))
+		goto out;
+	for (i = 0; i < eh.e_phnum; i++) {
+		if (pread(fd, &ph, sizeof(ph),
+			  eh.e_phoff + i * eh.e_phentsize) != sizeof(ph))
+			goto out;
+		if (ph.p_type != PT_INTERP)
+			continue;
+		if (!ph.p_filesz || ph.p_filesz > sz)
+			goto out;
+		if (pread(fd, out, ph.p_filesz, ph.p_offset) !=
+		    (ssize_t)ph.p_filesz)
+			goto out;
+		out[ph.p_filesz - 1] = '\0';
+		ret = 0;
+		break;
+	}
+out:
+	close(fd);
+	return ret;
+}
+
 #endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */
diff --git a/tools/testing/selftests/exec/binfmt_misc_loader.c b/tools/testing/selftests/exec/binfmt_misc_loader.c
new file mode 100644
index 000000000000..97fa0247ab53
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_loader.c
@@ -0,0 +1,363 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the 'L' (loader substitution) flag of binfmt_misc. A matched
+ * binary runs as the MAIN image - a fully native exec - with the
+ * registered interpreter substituted for its PT_INTERP. The payload
+ * (binfmt_loader_payload) asserts the native identity from inside.
+ *
+ * The substitute is a copy of the system loader found via our own
+ * PT_INTERP; magic matching pokes a marker into the ELF header's
+ * e_ident padding, which kernel and loader ignore.
+ *
+ * Needs root for the registration; no bpf toolchain involved.
+ */
+#define _GNU_SOURCE
+#include <elf.h>
+#include <link.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <sys/ptrace.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define ENTRY		"test_loader"
+#define MARKER		"LDRTST"
+#define INTERP_PATH	"/tmp/binfmt_loader_interp"
+#define MOVED_PATH	INTERP_PATH ".moved"
+#define TARGET_PATH	"/tmp/binfmt_loader_target.ldrtest"
+#define STATIC_PATH	"/tmp/binfmt_loader_static.ldrtest"
+#define FOREIGN_PATH	"/tmp/binfmt_loader_foreign.ldrtest"
+#define M_RULE		":" ENTRY ":M:9:" MARKER "::" INTERP_PATH ":L"
+#define E_RULE		":" ENTRY ":E::ldrtest::" INTERP_PATH ":L"
+#define FL_RULE		":" ENTRY ":E::ldrtest::" INTERP_PATH ":FL"
+
+/* Exit status run_target() reports when the exec was refused with ENOEXEC. */
+#define RUN_ENOEXEC	42
+
+/*
+ * Run @path with the canonical payload argv and return its exit status, or
+ * RUN_ENOEXEC when the exec itself was refused as unhandled.
+ */
+static int run_target(const char *path)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	if (pid == 0) {
+		execl(path, "payload-argv0", "argone", "argtwo", (char *)NULL);
+		_exit(errno == ENOEXEC ? RUN_ENOEXEC : 126);
+	}
+	if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+		return -1;
+	return WEXITSTATUS(status);
+}
+
+/* Execute the binary from an inaccessible O_CLOEXEC memfd. */
+static int run_memfd(const char *path)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	if (pid == 0) {
+		char *argv[] = { "payload-argv0", "argone", "argtwo", NULL };
+		char buf[4096];
+		int in, mfd;
+		ssize_t n;
+
+		mfd = memfd_create("loader-test", MFD_CLOEXEC);
+		in = open(path, O_RDONLY);
+		if (mfd < 0 || in < 0)
+			_exit(125);
+		while ((n = read(in, buf, sizeof(buf))) > 0)
+			if (write(mfd, buf, n) != n)
+				_exit(125);
+		close(in);
+		setenv("BINFMT_TEST_MEMFD", "1", 1);
+		unsetenv("BINFMT_TEST_BINARY");
+		syscall(SYS_execveat, mfd, "", argv, environ, AT_EMPTY_PATH);
+		_exit(126);
+	}
+	if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+		return -1;
+	return WEXITSTATUS(status);
+}
+
+static int stat_codes(pid_t pid, unsigned long *start_code,
+		      unsigned long *end_code)
+{
+	char buf[4096], path[64], *p;
+	ssize_t n;
+	int fd, i;
+
+	snprintf(path, sizeof(path), "/proc/%d/stat", pid);
+	fd = open(path, O_RDONLY);
+	if (fd < 0)
+		return -1;
+	n = read(fd, buf, sizeof(buf) - 1);
+	close(fd);
+	if (n <= 0)
+		return -1;
+	buf[n] = '\0';
+	p = strrchr(buf, ')');
+	if (!p)
+		return -1;
+	p++;
+	for (i = 0; i < 23; i++) {
+		p = strchr(p + 1, ' ');
+		if (!p)
+			return -1;
+	}
+	if (sscanf(p, " %lu %lu", start_code, end_code) != 2)
+		return -1;
+	return 0;
+}
+
+/*
+ * The differentiator against the transparent mode: at PTRACE_EVENT_EXEC
+ * the identity is already complete - exe, auxv and the stat code markers
+ * are mutually consistent with no window a debugger could observe.
+ */
+static int ptrace_probe(const char *target)
+{
+	unsigned long auxv[2 * 64], base = 0, entry = 0, at_flags = 0;
+	unsigned long start_code = 0, end_code = 0;
+	int status, fd, execfd_seen = 0, failed = 0;
+	char path[64], buf[PATH_MAX];
+	ssize_t n;
+	pid_t pid;
+	int i;
+
+	pid = fork();
+	if (pid == 0) {
+		ptrace(PTRACE_TRACEME, 0, NULL, NULL);
+		raise(SIGSTOP);
+		execl(target, "payload-argv0", "argone", "argtwo", (char *)NULL);
+		_exit(126);
+	}
+	if (pid < 0)
+		return -1;
+	if (waitpid(pid, &status, 0) != pid || !WIFSTOPPED(status))
+		goto fail_kill;
+	if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)PTRACE_O_TRACEEXEC))
+		goto fail_kill;
+	if (ptrace(PTRACE_CONT, pid, NULL, NULL))
+		goto fail_kill;
+	if (waitpid(pid, &status, 0) != pid || !WIFSTOPPED(status) ||
+	    status >> 8 != (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) {
+		fprintf(stderr, "no exec stop (status %#x)\n", status);
+		goto fail_kill;
+	}
+
+	snprintf(path, sizeof(path), "/proc/%d/exe", pid);
+	n = readlink(path, buf, sizeof(buf) - 1);
+	if (n <= 0) {
+		failed = 1;
+	} else {
+		buf[n] = '\0';
+		if (strcmp(buf, target)) {
+			fprintf(stderr, "exe at exec stop: %s\n", buf);
+			failed = 1;
+		}
+	}
+
+	snprintf(path, sizeof(path), "/proc/%d/auxv", pid);
+	fd = open(path, O_RDONLY);
+	if (fd < 0) {
+		failed = 1;
+		n = 0;
+	} else {
+		n = read(fd, auxv, sizeof(auxv));
+		close(fd);
+	}
+	for (i = 0; i + 1 < (int)(n / sizeof(unsigned long)); i += 2) {
+		switch (auxv[i]) {
+		case AT_BASE:
+			base = auxv[i + 1];
+			break;
+		case AT_ENTRY:
+			entry = auxv[i + 1];
+			break;
+		case AT_FLAGS:
+			at_flags = auxv[i + 1];
+			break;
+		case AT_EXECFD:
+			execfd_seen = 1;
+			break;
+		}
+	}
+
+	if (stat_codes(pid, &start_code, &end_code))
+		failed = 1;
+
+	if (!base || execfd_seen || at_flags) {
+		fprintf(stderr, "auxv at exec stop not native\n");
+		failed = 1;
+	}
+	if (!start_code || entry < start_code || entry >= end_code) {
+		fprintf(stderr, "auxv/stat inconsistent at exec stop\n");
+		failed = 1;
+	}
+
+	if (ptrace(PTRACE_CONT, pid, NULL, NULL))
+		goto fail_kill;
+	if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
+	    WEXITSTATUS(status))
+		failed = 1;
+	return failed ? -1 : 0;
+
+fail_kill:
+	kill(pid, SIGKILL);
+	waitpid(pid, &status, 0);
+	return -1;
+}
+
+FIXTURE(loader) {
+	bool have_static;
+};
+
+FIXTURE_SETUP(loader)
+{
+	unsigned short foreign_machine = 0xdead;
+	char src[PATH_MAX], loader[PATH_MAX];
+
+	if (getuid() != 0)
+		SKIP(return, "test must be run as root");
+	if (!binfmt_misc_available())
+		SKIP(return, "no binfmt_misc");
+	if (find_loader(loader, sizeof(loader)))
+		SKIP(return, "cannot determine own PT_INTERP");
+
+	ASSERT_EQ(copy_file(loader, INTERP_PATH), 0);
+
+	ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0);
+	ASSERT_EQ(copy_file(src, TARGET_PATH), 0);
+	ASSERT_EQ(patch_file(TARGET_PATH, EI_PAD, MARKER, strlen(MARKER)), 0);
+
+	/* The same payload with a machine type this kernel cannot load. */
+	ASSERT_EQ(copy_file(src, FOREIGN_PATH), 0);
+	ASSERT_EQ(patch_file(FOREIGN_PATH, EI_PAD, MARKER, strlen(MARKER)), 0);
+	ASSERT_EQ(patch_file(FOREIGN_PATH, offsetof(ElfW(Ehdr), e_machine),
+			     &foreign_machine, sizeof(foreign_machine)), 0);
+
+	self->have_static =
+		artifact_path(src, sizeof(src), "binfmt_loader_payload_static") == 0 &&
+		copy_file(src, STATIC_PATH) == 0;
+
+	setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1);
+	setenv("BINFMT_TEST_INTERP", INTERP_PATH, 1);
+
+	/* Everything below needs the flag; find out once. */
+	if (write_reg(E_RULE)) {
+		ASSERT_EQ(errno, EINVAL);
+		SKIP(return, "kernel without the 'L' flag");
+	}
+	unregister(ENTRY);
+}
+
+FIXTURE_TEARDOWN(loader)
+{
+	unregister(ENTRY);
+	if (access(MOVED_PATH, F_OK) == 0)
+		rename(MOVED_PATH, INTERP_PATH);
+	unlink(TARGET_PATH);
+	unlink(STATIC_PATH);
+	unlink(FOREIGN_PATH);
+	unlink(INTERP_PATH);
+}
+
+/* Grammar sanity check: the same entry without 'L' has to register. */
+TEST_F(loader, plain_entry_registers)
+{
+	ASSERT_EQ(write_reg(":" ENTRY ":E::ldrtest::" INTERP_PATH ":"), 0);
+}
+
+/* 'L' is a native exec: every classic-dispatch flag is rejected. */
+TEST_F(loader, rejects_classic_flags)
+{
+	static const char * const combos[] = { "LT", "LP", "LC", "LO" };
+	char rule[PATH_MAX];
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(combos); i++) {
+		int rc;
+
+		snprintf(rule, sizeof(rule),
+			 ":" ENTRY ":E::ldrtest::" INTERP_PATH ":%s", combos[i]);
+		rc = write_reg(rule);
+		EXPECT_EQ(rc, -1)
+			TH_LOG("'%s' was not rejected", combos[i]);
+		if (rc == 0) {
+			unregister(ENTRY);
+			continue;
+		}
+		EXPECT_EQ(errno, EINVAL);
+	}
+}
+
+TEST_F(loader, extension_matched)
+{
+	ASSERT_EQ(write_reg(E_RULE), 0);
+	EXPECT_EQ(run_target(TARGET_PATH), 0);
+}
+
+TEST_F(loader, magic_matched)
+{
+	ASSERT_EQ(write_reg(M_RULE), 0);
+	EXPECT_EQ(run_target(TARGET_PATH), 0);
+}
+
+/*
+ * The differentiator against the transparent mode: at PTRACE_EVENT_EXEC the
+ * identity is already complete, with no window a debugger could observe.
+ */
+TEST_F(loader, exec_stop_consistency)
+{
+	ASSERT_EQ(write_reg(E_RULE), 0);
+	EXPECT_EQ(ptrace_probe(TARGET_PATH), 0);
+}
+
+/* A binary without PT_INTERP drops the override and runs natively. */
+TEST_F(loader, static_binary_runs_natively)
+{
+	if (!self->have_static)
+		SKIP(return, "no static payload built");
+
+	ASSERT_EQ(write_reg(E_RULE), 0);
+	setenv("BINFMT_TEST_BINARY", STATIC_PATH, 1);
+	setenv("BINFMT_TEST_STATIC", "1", 1);
+	EXPECT_EQ(run_target(STATIC_PATH), 0);
+	unsetenv("BINFMT_TEST_STATIC");
+	setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1);
+}
+
+/* Nothing needs the binary's path, so an inaccessible fd works. */
+TEST_F(loader, inaccessible_memfd)
+{
+	ASSERT_EQ(write_reg(M_RULE), 0);
+	EXPECT_EQ(run_memfd(TARGET_PATH), 0);
+}
+
+/* The whole exec of a wrong-arch binary fails as if unhandled. */
+TEST_F(loader, foreign_arch_enoexec)
+{
+	ASSERT_EQ(write_reg(M_RULE), 0);
+	EXPECT_EQ(run_target(FOREIGN_PATH), RUN_ENOEXEC);
+}
+
+/* 'F' pre-opens the substitute, so it survives losing its path. */
+TEST_F(loader, fixed_interpreter_survives_rename)
+{
+	ASSERT_EQ(write_reg(FL_RULE), 0);
+	ASSERT_EQ(rename(INTERP_PATH, MOVED_PATH), 0);
+	EXPECT_EQ(run_target(TARGET_PATH), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/loader.bpf.c b/tools/testing/selftests/exec/loader.bpf.c
new file mode 100644
index 000000000000..108e51dd4961
--- /dev/null
+++ b/tools/testing/selftests/exec/loader.bpf.c
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binfmt_misc_ops handler for the loader-substitution case: match the
+ * marker the harness poked into the payload's e_ident padding and ask for
+ * the selected interpreter to be substituted for the binary's PT_INTERP,
+ * so the binary itself runs as a fully native exec.
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+#define EI_CLASS	4
+#define EI_PAD		9
+#define ELFCLASS64	2
+
+extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
+				 size_t path__sz) __ksym;
+extern int bpf_binprm_set_flags(struct linux_binprm *bprm,
+				enum bpf_binprm_flags flags) __ksym;
+
+SEC("struct_ops.s/match")
+bool BPF_PROG(loader_match, struct linux_binprm *bprm)
+{
+	if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
+	    bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
+	    bprm->buf[EI_CLASS] != ELFCLASS64)
+		return false;
+
+	/* The harness marks the payload with "LDRTST" at EI_PAD. */
+	return bprm->buf[EI_PAD + 0] == 'L' && bprm->buf[EI_PAD + 1] == 'D' &&
+	       bprm->buf[EI_PAD + 2] == 'R' && bprm->buf[EI_PAD + 3] == 'T' &&
+	       bprm->buf[EI_PAD + 4] == 'S' && bprm->buf[EI_PAD + 5] == 'T';
+}
+
+SEC("struct_ops.s/load")
+int BPF_PROG(loader_load, struct linux_binprm *bprm)
+{
+	char interp[] = "/tmp/binfmt_loader_interp";
+	int err;
+
+	err = bpf_binprm_set_flags(bprm, BPF_BINPRM_LOADER);
+	if (err)
+		return err;
+
+	/* @path__sz includes the terminating NUL; 0 commits the selection. */
+	return bpf_binprm_set_interp(bprm, interp, sizeof(interp));
+}
+
+SEC(".struct_ops.link")
+struct binfmt_misc_ops loader = {
+	.match = (void *)loader_match,
+	.load = (void *)loader_load,
+	.name = "loader",
+};

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH 21/21] binfmt_misc: document loader substitution
  2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
                   ` (19 preceding siblings ...)
  2026-07-20  9:33 ` [PATCH 20/21] selftests/exec: test binfmt_misc " Christian Brauner
@ 2026-07-20  9:33 ` Christian Brauner
  20 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20  9:33 UTC (permalink / raw)
  To: Farid Zakaria, linux-fsdevel
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-mm, bpf, jannh, mail,
	Christian Brauner (Amutable)

Describe the L mode next to the transparent one.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst | 50 +++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 8c861bf0c56c..b468a148cdab 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -100,6 +100,13 @@ Here is what the fields mean:
             ``AT_FLAGS_TRANSPARENT_INTERP`` contract. Combining ``T``
             with ``P`` is rejected: transparency preserves the whole
             argument vector, argv[0] included.
+      ``L`` - loader substitution
+            Do not run the interpreter on the binary at all: load the
+            binary itself as a fully native exec and substitute the
+            interpreter for the loader named in the binary's
+            ``PT_INTERP``. See the "Loader substitution" section
+            below. ``L`` rejects ``T``, ``P``, ``O`` and ``C``;
+            ``F`` composes.
 
 
 There are some restrictions:
@@ -246,6 +253,49 @@ execution is the address space layout. The interpreter occupies the main-image
 position and the program lives in the mmap region.
 
 
+Loader substitution
+-------------------
+
+The ``L`` flag turns the execution model around. Instead of running the
+registered interpreter with the binary as its payload the kernel loads
+the matched binary itself as the main image and substitutes the registered
+interpreter for the loader named in the binary's ``PT_INTERP``.
+
+Because the exec is native, there is no dispatch identity to
+reconstruct and no contract the substitute has to implement. A stock
+dynamic loader works unchanged. The argument vector is untouched,
+credentials and ``AT_SECURE`` derive from the binary, there is no
+``AT_EXECFD`` and no marker in the aux vector, the binary sits in the
+main-image slot with the native brk placement so ``/proc/pid/maps``,
+core dumps and perf mmap records have the native shape, and the
+identity is already complete when ``PTRACE_EVENT_EXEC`` stops the
+tracee. So launching under a debugger works, not just attaching. ``L``
+entries are for ELF binaries of a native architecture. Foreign-arch
+emulation and non-ELF payloads remain the domain of the classic and
+transparent modes.
+
+The override applies when the format that finally claims the file is
+ELF with a ``PT_INTERP``. A matched binary without one or an
+interpreter-less ``ET_DYN`` drops the override and runs natively. A
+file claimed by another format is handled by that format as if the entry
+had not matched. A format that cannot consume the override at
+all instead refuses the exec with ``ENOEXEC`` before the point of no
+return, so the substitution is never silently ignored where it would
+have applied.
+
+A wrong-architecture ELF fails the whole exec with ``ENOEXEC`` exactly
+as if no entry had matched. A substitute that is not ELF of the right
+architecture fails with ``ELIBBAD``. The usual ``PT_INTERP`` sanity
+checks on the binary still apply. But the segment's content is otherwise
+irrelevant.
+
+``L`` rejects the classic-dispatch flags ``T``, ``P``, ``O`` and ``C``
+at registration. ``F`` composes and is valuable. The substitute is
+opened at registration time, so later mount namespace or path changes
+cannot redirect it. As with ``C``, register only trusted interpreters.
+The substituted loader runs with credentials derived from the binary.
+
+
 Hints
 -----
 

-- 
2.53.0


^ permalink raw reply related	[flat|nested] 28+ messages in thread

* Re: [PATCH 01/21] exec: do not act on a stale execfd request without an executable
  2026-07-20  9:33 ` [PATCH 01/21] exec: do not act on a stale execfd request without an executable Christian Brauner
@ 2026-07-20 10:00   ` sashiko-bot
  2026-07-20 11:30     ` Christian Brauner
  0 siblings, 1 reply; 28+ messages in thread
From: sashiko-bot @ 2026-07-20 10:00 UTC (permalink / raw)
  To: Christian Brauner; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the patch fails to fix it completely. It fails to clear leaked `have_execfd` and `execfd_creds` flags if the fallback format uses an interpreter (e.g., `binfmt_script`), potentially allowing setuid script execution.
--

--- Patch [1]: [PATCH 01/21] exec: do not act on a stale execfd request without an executable ---
commit 9dcfa7daaba6407f79aa884e4fa9f0bc5d755870
Author: Christian Brauner <brauner@kernel.org>

exec: do not act on a stale execfd request without an executable

This commit addresses an issue where binfmt_misc leaves bprm->have_execfd
and bprm->execfd_creds set when it fails to open an interpreter. It clears
these flags in begin_new_exec() if bprm->executable is NULL to prevent a
subsequent NULL pointer dereference.

> diff --git a/fs/exec.c b/fs/exec.c
> index c698aabe9abd0..2f50aa0398278 100644
> --- a/fs/exec.c
> +++ b/fs/exec.c
> @@ -1112,6 +1112,12 @@ int begin_new_exec(struct linux_binprm * bprm)
>  	struct task_struct *me = current;
>  	int retval;
>  
> +	/* A declined execfd request has no executable for a later format. */
> +	if (!bprm->executable) {
> +		bprm->have_execfd = 0;
> +		bprm->execfd_creds = 0;
> +	}

[Severity: High]
This isn't a bug introduced by this patch, but does this check completely
resolve the leaked flags issue?

If load_misc_binary() leaks the have_execfd flag and the kernel falls back to
binfmt_script, it looks like exec_binprm() might unconditionally consume the
leaked flag and set bprm->executable to the script:

fs/exec.c:exec_binprm() {
    ...
    if (unlikely(bprm->have_execfd)) {
        if (bprm->executable) {
            fput(exec);
            return -ENOEXEC;
        }
        bprm->executable = exec;
    } else
    ...
}

In the case of a setuid script with a statically linked interpreter (like
#!/sbin/busybox.static sh), would this cause begin_new_exec() to see a
non-NULL bprm->executable?

If so, would it bypass this cleanup and compute credentials from the script
rather than the interpreter, potentially honoring the setuid bit on the script?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260720-work-bpf-binfmt_misc-ptinterp-v1-0-ddb76c9a508e@kernel.org?part=1

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 01/21] exec: do not act on a stale execfd request without an executable
  2026-07-20 10:00   ` sashiko-bot
@ 2026-07-20 11:30     ` Christian Brauner
  2026-07-20 12:44       ` Christian Brauner
  0 siblings, 1 reply; 28+ messages in thread
From: Christian Brauner @ 2026-07-20 11:30 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf

On Mon, Jul 20, 2026 at 10:00:22AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> 
> Pre-existing issues:
> - [High] This problem wasn't introduced by this patch, but the patch fails to fix it completely. It fails to clear leaked `have_execfd` and `execfd_creds` flags if the fallback format uses an interpreter (e.g., `binfmt_script`), potentially allowing setuid script execution.
> --
> 
> --- Patch [1]: [PATCH 01/21] exec: do not act on a stale execfd request without an executable ---
> commit 9dcfa7daaba6407f79aa884e4fa9f0bc5d755870
> Author: Christian Brauner <brauner@kernel.org>
> 
> exec: do not act on a stale execfd request without an executable
> 
> This commit addresses an issue where binfmt_misc leaves bprm->have_execfd
> and bprm->execfd_creds set when it fails to open an interpreter. It clears
> these flags in begin_new_exec() if bprm->executable is NULL to prevent a
> subsequent NULL pointer dereference.
> 
> > diff --git a/fs/exec.c b/fs/exec.c
> > index c698aabe9abd0..2f50aa0398278 100644
> > --- a/fs/exec.c
> > +++ b/fs/exec.c
> > @@ -1112,6 +1112,12 @@ int begin_new_exec(struct linux_binprm * bprm)
> >  	struct task_struct *me = current;
> >  	int retval;
> >  
> > +	/* A declined execfd request has no executable for a later format. */
> > +	if (!bprm->executable) {
> > +		bprm->have_execfd = 0;
> > +		bprm->execfd_creds = 0;
> > +	}
> 
> [Severity: High]
> This isn't a bug introduced by this patch, but does this check completely
> resolve the leaked flags issue?
> 
> If load_misc_binary() leaks the have_execfd flag and the kernel falls back to
> binfmt_script, it looks like exec_binprm() might unconditionally consume the
> leaked flag and set bprm->executable to the script:
> 
> fs/exec.c:exec_binprm() {
>     ...
>     if (unlikely(bprm->have_execfd)) {
>         if (bprm->executable) {
>             fput(exec);
>             return -ENOEXEC;
>         }
>         bprm->executable = exec;
>     } else
>     ...
> }
> 
> In the case of a setuid script with a statically linked interpreter (like
> #!/sbin/busybox.static sh), would this cause begin_new_exec() to see a
> non-NULL bprm->executable?
> 
> If so, would it bypass this cleanup and compute credentials from the script
> rather than the interpreter, potentially honoring the setuid bit on the script?

This is the same finding I had basically. It means we need to clear this
where the flags are set. That needs to be backported to a lot of older
kernels so we need it right at the beginning of everything that is in
vfs-7.3.binfmt... I'll send that as a standalone fix in a bit.

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode
  2026-07-20  9:33 ` [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode Christian Brauner
@ 2026-07-20 11:44   ` sashiko-bot
  2026-07-20 12:17     ` Christian Brauner
  0 siblings, 1 reply; 28+ messages in thread
From: sashiko-bot @ 2026-07-20 11:44 UTC (permalink / raw)
  To: Christian Brauner; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Stack-based buffer overflow in binfmt_transparent_interp when concatenating strings into the `expect` array.
--

--- Patch [14]: [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode ---
commit 9d915ef86186ae5a0a73c74e07d46a93d08f7fe5
Author: Christian Brauner <brauner@kernel.org>

selftests/exec: test the transparent binfmt_misc mode

This commit introduces a transparent dispatch test for binfmt_misc from both
activation paths. The dispatch routes to a shared asserting interpreter that
validates the kernel's constructed identity from inside the process.

> diff --git a/tools/testing/selftests/exec/binfmt_transparent_interp.c b/tools/testing/selftests/exec/binfmt_transparent_interp.c
> new file mode 100644
> index 0000000000000..2a71c47067582
> --- /dev/null
> +++ b/tools/testing/selftests/exec/binfmt_transparent_interp.c
> @@ -0,0 +1,130 @@
[ ... ]
> +int main(int argc, char **argv)
> +{
> +	const char *binary = getenv("BINFMT_TEST_BINARY");
> +	const char *argv0 = getenv("BINFMT_TEST_ARGV0");
> +	char exe[PATH_MAX], real[PATH_MAX], buf[PATH_MAX];
> +	char expect[PATH_MAX + 32];
[ ... ]
> +	/* Distinct from the binary path, so a classic argv splice is caught. */
> +	want[0] = argv0 ? argv0 : binary;
> +	want[1] = "argone";
> +	want[2] = "argtwo";
[ ... ]
> +	/* And so did the kernel's copy of it: the same strings, NUL separated. */
> +	for (i = 0, expect_len = 0; i < ARRAY_SIZE(want); i++) {
> +		size_t len = strlen(want[i]) + 1;
> +
> +		memcpy(expect + expect_len, want[i], len);

[Severity: Medium]
Could this overflow the expect[] buffer?

If the user executes this test program with BINFMT_TEST_ARGV0 or 
BINFMT_TEST_BINARY set to a string longer than PATH_MAX, expect_len + len 
could exceed the size of the expect[] buffer. Since there is no bounds 
checking before the memcpy(), this would result in a stack-based buffer 
overflow.

> +		expect_len += len;
> +	}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260720-work-bpf-binfmt_misc-ptinterp-v1-0-ddb76c9a508e@kernel.org?part=14

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode
  2026-07-20 11:44   ` sashiko-bot
@ 2026-07-20 12:17     ` Christian Brauner
  0 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20 12:17 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf

On Mon, Jul 20, 2026 at 11:44:46AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] Stack-based buffer overflow in binfmt_transparent_interp when concatenating strings into the `expect` array.
> --
> 
> --- Patch [14]: [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode ---
> commit 9d915ef86186ae5a0a73c74e07d46a93d08f7fe5
> Author: Christian Brauner <brauner@kernel.org>
> 
> selftests/exec: test the transparent binfmt_misc mode
> 
> This commit introduces a transparent dispatch test for binfmt_misc from both
> activation paths. The dispatch routes to a shared asserting interpreter that
> validates the kernel's constructed identity from inside the process.
> 
> > diff --git a/tools/testing/selftests/exec/binfmt_transparent_interp.c b/tools/testing/selftests/exec/binfmt_transparent_interp.c
> > new file mode 100644
> > index 0000000000000..2a71c47067582
> > --- /dev/null
> > +++ b/tools/testing/selftests/exec/binfmt_transparent_interp.c
> > @@ -0,0 +1,130 @@
> [ ... ]
> > +int main(int argc, char **argv)
> > +{
> > +	const char *binary = getenv("BINFMT_TEST_BINARY");
> > +	const char *argv0 = getenv("BINFMT_TEST_ARGV0");
> > +	char exe[PATH_MAX], real[PATH_MAX], buf[PATH_MAX];
> > +	char expect[PATH_MAX + 32];
> [ ... ]
> > +	/* Distinct from the binary path, so a classic argv splice is caught. */
> > +	want[0] = argv0 ? argv0 : binary;
> > +	want[1] = "argone";
> > +	want[2] = "argtwo";
> [ ... ]
> > +	/* And so did the kernel's copy of it: the same strings, NUL separated. */
> > +	for (i = 0, expect_len = 0; i < ARRAY_SIZE(want); i++) {
> > +		size_t len = strlen(want[i]) + 1;
> > +
> > +		memcpy(expect + expect_len, want[i], len);
> 
> [Severity: Medium]
> Could this overflow the expect[] buffer?
> 
> If the user executes this test program with BINFMT_TEST_ARGV0 or 
> BINFMT_TEST_BINARY set to a string longer than PATH_MAX, expect_len + len 
> could exceed the size of the expect[] buffer. Since there is no bounds 
> checking before the memcpy(), this would result in a stack-based buffer 
> overflow.

It's a selftest and the callers just don't do that. So let's not worry
about it.

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 20/21] selftests/exec: test binfmt_misc loader substitution
  2026-07-20  9:33 ` [PATCH 20/21] selftests/exec: test binfmt_misc " Christian Brauner
@ 2026-07-20 12:22   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-20 12:22 UTC (permalink / raw)
  To: Christian Brauner; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing NULL check for the map pointer returned by `bpf_object__find_map_by_name` before passing it to `bpf_map__attach_struct_ops`.
- [Medium] An unhandled `read()` error on `/proc/<pid>/auxv` leads to a massive out-of-bounds array read due to implicit type conversion on 32-bit architectures.
--

--- Patch [20]: [PATCH 20/21] selftests/exec: test binfmt_misc loader substitution ---
commit 7fb50e082768e7f213f616cf84985ff4feb60ccd
Author: Christian Brauner <brauner@kernel.org>
Subject: selftests/exec: test binfmt_misc loader substitution

This commit exercises the 'L' flag end to end by running a payload as the main
image with a copy of the system loader substituted for its PT_INTERP. It also
introduces a bpf-side BPF_BINPRM_LOADER test path.

> diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
> index 483c46e134107..926db50d39d54 100644
> --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
> +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c

[ ... ]

> +static int bpf_case_start(struct bpf_case *c, const char *objfile,
> +			  const char *handler, const char *entry)
>  {
> -	struct bpf_object *obj;
> -	struct bpf_map *map;
> -	struct bpf_link *link;
> -	int ret = -1;
> +	c->obj = NULL;
> +	c->link = NULL;
> +	c->entry = entry;
>  
> -	obj = bpf_object__open_file(objfile, NULL);
> -	if (!obj || libbpf_get_error(obj)) {
> +	c->obj = bpf_object__open_file(objfile, NULL);
> +	if (!c->obj || libbpf_get_error(c->obj)) {
>  		fprintf(stderr, "open %s failed\n", objfile);
> +		c->obj = NULL;
>  		return -1;
>  	}
> -	if (bpf_object__load(obj)) {
> +	if (bpf_object__load(c->obj)) {
>  		fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n",
>  			objfile);
> -		goto close;
> -	}
> -	map = bpf_object__find_map_by_name(obj, handler);
> -	if (!map) {
> -		fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile);
> -		goto close;
> +		goto fail;
>  	}
> -	link = bpf_map__attach_struct_ops(map);
> -	if (!link || libbpf_get_error(link)) {
> +	c->link = bpf_map__attach_struct_ops(
> +			bpf_object__find_map_by_name(c->obj, handler));
> +	if (!c->link || libbpf_get_error(c->link)) {

[Severity: Medium]
Is it possible for bpf_object__find_map_by_name() to return NULL here if the 
map isn't found?

If it does return NULL, passing it directly to bpf_map__attach_struct_ops()
will cause a segmentation fault because the attach API unconditionally 
dereferences the map pointer to check its type. The original code included a
check for a NULL map before attempting to attach.

>  		fprintf(stderr, "attach struct_ops '%s' failed\n", handler);
> -		goto close;
> +		c->link = NULL;
> +		goto fail;
>  	}

[ ... ]

> diff --git a/tools/testing/selftests/exec/binfmt_misc_loader.c b/tools/testing/selftests/exec/binfmt_misc_loader.c
> new file mode 100644
> index 0000000000000..97fa0247ab53e
> --- /dev/null
> +++ b/tools/testing/selftests/exec/binfmt_misc_loader.c

[ ... ]

> +static int ptrace_probe(const char *target)
> +{
> +	unsigned long auxv[2 * 64], base = 0, entry = 0, at_flags = 0;
> +	unsigned long start_code = 0, end_code = 0;
> +	int status, fd, execfd_seen = 0, failed = 0;
> +	char path[64], buf[PATH_MAX];
> +	ssize_t n;
> +	pid_t pid;
> +	int i;

[ ... ]

> +	snprintf(path, sizeof(path), "/proc/%d/auxv", pid);
> +	fd = open(path, O_RDONLY);
> +	if (fd < 0) {
> +		failed = 1;
> +		n = 0;
> +	} else {
> +		n = read(fd, auxv, sizeof(auxv));
> +		close(fd);
> +	}
> +	for (i = 0; i + 1 < (int)(n / sizeof(unsigned long)); i += 2) {

[Severity: Medium]
What happens if the read() call fails and returns -1?

If n is set to -1, the loop condition implicitly converts it to an unsigned
size_t during the division: n / sizeof(unsigned long). On 32-bit platforms,
this division results in a massive positive integer (0x3FFFFFFF). 

Because this is cast to an int, the loop will iterate far past the end of the
auxv array, leading to an out-of-bounds memory read. Should this check if n
is greater than 0 before proceeding to the loop?

> +		switch (auxv[i]) {
> +		case AT_BASE:
> +			base = auxv[i + 1];
> +			break;

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260720-work-bpf-binfmt_misc-ptinterp-v1-0-ddb76c9a508e@kernel.org?part=20

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH 01/21] exec: do not act on a stale execfd request without an executable
  2026-07-20 11:30     ` Christian Brauner
@ 2026-07-20 12:44       ` Christian Brauner
  0 siblings, 0 replies; 28+ messages in thread
From: Christian Brauner @ 2026-07-20 12:44 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf

> This is the same finding I had basically. It means we need to clear this
> where the flags are set. That needs to be backported to a lot of older
> kernels so we need it right at the beginning of everything that is in
> vfs-7.3.binfmt... I'll send that as a standalone fix in a bit.

This is now a separate fix on vfs.fixes. So this patch will just be
dropped from the series.

^ permalink raw reply	[flat|nested] 28+ messages in thread

end of thread, other threads:[~2026-07-20 12:44 UTC | newest]

Thread overview: 28+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-20  9:33 [PATCH 00/21] binfmt_misc: transparent interpreters and PT_INTERP loader substitution Christian Brauner
2026-07-20  9:33 ` [PATCH 01/21] exec: do not act on a stale execfd request without an executable Christian Brauner
2026-07-20 10:00   ` sashiko-bot
2026-07-20 11:30     ` Christian Brauner
2026-07-20 12:44       ` Christian Brauner
2026-07-20  9:33 ` [PATCH 02/21] docs, binfmt_misc: keep general usage out of the handler sections Christian Brauner
2026-07-20  9:33 ` [PATCH 03/21] binfmt_misc: table-drive the register string flags Christian Brauner
2026-07-20  9:33 ` [PATCH 04/21] binfmt_misc: normalize the per-exec invocation flags Christian Brauner
2026-07-20  9:33 ` [PATCH 05/21] binfmt_misc: split out entry_open_interpreter() Christian Brauner
2026-07-20  9:33 ` [PATCH 06/21] binfmt_misc: split out build_interp_argv() Christian Brauner
2026-07-20  9:33 ` [PATCH 07/21] exec: release the replaced file with do_close_execat() Christian Brauner
2026-07-20  9:33 ` [PATCH 08/21] exec: add AT_FLAGS_TRANSPARENT_INTERP Christian Brauner
2026-07-20  9:33 ` [PATCH 09/21] exec: label mm->exe_file with the binary for a transparent dispatch Christian Brauner
2026-07-20  9:33 ` [PATCH 10/21] binfmt_misc: add transparent interpreter dispatch Christian Brauner
2026-07-20  9:33 ` [PATCH 11/21] binfmt_misc: add a static transparent flag 'T' Christian Brauner
2026-07-20  9:33 ` [PATCH 12/21] binfmt_misc: let a bpf handler run the interpreter transparently Christian Brauner
2026-07-20  9:33 ` [PATCH 13/21] selftests/exec: convert the binfmt_misc bpf test to the kselftest harness Christian Brauner
2026-07-20  9:33 ` [PATCH 14/21] selftests/exec: test the transparent binfmt_misc mode Christian Brauner
2026-07-20 11:44   ` sashiko-bot
2026-07-20 12:17     ` Christian Brauner
2026-07-20  9:33 ` [PATCH 15/21] binfmt_misc: document the transparent identity contract Christian Brauner
2026-07-20  9:33 ` [PATCH 16/21] exec: carry a PT_INTERP substitute in struct linux_binprm Christian Brauner
2026-07-20  9:33 ` [PATCH 17/21] binfmt_elf: consume a stashed PT_INTERP substitute Christian Brauner
2026-07-20  9:33 ` [PATCH 18/21] binfmt_misc: add the 'L' loader substitution flag Christian Brauner
2026-07-20  9:33 ` [PATCH 19/21] binfmt_misc: let a bpf handler request loader substitution Christian Brauner
2026-07-20  9:33 ` [PATCH 20/21] selftests/exec: test binfmt_misc " Christian Brauner
2026-07-20 12:22   ` sashiko-bot
2026-07-20  9:33 ` [PATCH 21/21] binfmt_misc: document " Christian Brauner

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox