Linux filesystem development
 help / color / mirror / Atom feed
* [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
@ 2026-07-14 19:58 Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm Christian Brauner
                   ` (9 more replies)
  0 siblings, 10 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

This is a POC for the nix people and Farid and Eric in particular. I
would take my hands off the wheel now that I POCed this and hand it to
Farid if he likes to take it forward.

VL;MR (very long, must read):

For a while now Farid has been trying to make relocatable, hermetic
binaries (think Nix-style store layouts) work without patchelf tricks
or wrapper scripts. For such binaries the right dynamic loader can only
be determined relative to the location of the binary itself, which
neither PT_INTERP nor a fixed binfmt_misc interpreter string can
express.

The first attempt was $ORIGIN expansion in PT_INTERP [1]. I pushed back
on that. Userspace guards $ORIGIN behind AT_SECURE so the kernel would
have to make the used loader depend on the type of binary, LSMs would
need a say, it changes long-standing behavior in ways that are ripe for
loader injection attacks, and bprm->file may not have a usable path at
all (memfds, deleted files, unresolvable paths). Making the kernel
splice bprm->file back together with PT_INTERP is terrible. The second
attempt was a pluggable ELF interpreter loader registry [2] which would
mean actual kernel modules for custom binary formats. Also no.
binfmt_misc was invented to kill exactly this horrendous past.

What I suggested instead [3] was to put this where delegating binary
formats to userspace already lives: binfmt_misc. The only things
binfmt_misc cannot do today are matching programmatically and computing
the interpreter per binary instead of using a fixed string recorded at
registration time. Farid prototyped that with an eBPF program [4] and
it turned out quite workable, but the prototype ran a SOCKET_FILTER
program over bprm->buf, added a new helper to the frozen uapi helper
list, and returned the computed path through per-CPU memory.

This series is the proposal turned into what I think the bpf side
{c,sh}ould actually look like. It is a POC: it builds, the selftests
pass, and the design is what I want to discuss. The selftests are
Farid's from his v2 posting, adapted to the contract below.

A handler is an instance of the new binfmt_misc_ops struct_ops with a
name and two ops:

	struct binfmt_misc_ops {
		bool (*match)(struct linux_binprm *bprm);
		int (*load)(struct linux_binprm *bprm);
		char name[BINFMT_MISC_OPS_NAME_MAX];
	};

Both programs receive the bprm as a trusted BTF pointer and both are
sleepable. The match program decides from the entry lookup walk whether
the handler applies, under the same rules as magic matching:
registration order, first match wins. It is not limited to the
prefetched 256 bytes in bprm->buf: it can read arbitrary file content
through bpf_dynptr_from_file(), e.g. to find an ELF interpreter
segment at whatever offset it sits. That is what makes multiple
independent handlers workable at all - a handler that cannot read the
file would have to match broadly and reject from its load program,
stealing the binaries of every handler registered after it. To make
this safe the entry walk becomes an SRCU read-side section. The load
program of the matched handler then selects the interpreter, reading
the file the same way and resolving the binary's location via
bpf_path_d_path() on &bprm->file->f_path. That also solves the
prototype's limitation of only seeing the first 256 bytes of the file.
Selecting is the load program's privilege: the verifier rejects the
selection kfuncs in match, keyed off the struct_ops member a program
attaches to. A match commits the exec to the handler: a failing load
fails the exec instead of falling through to later entries, with
-ENOEXEC handing over to the remaining binary formats, so the walk is
never left and re-entered.

The genuinely new piece of bpf surface is a small family of kfuncs:

	int bpf_binprm_set_interp(struct linux_binprm *bprm,
				  const char *path, size_t path__sz);
	int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
				      const char *arg, size_t arg__sz);
	int bpf_binprm_set_flags(struct linux_binprm *bprm,
				 enum bpf_binprm_flags flags);

staging the selected interpreter, an optional single argument for it
(the slot the optional argument of a #! interpreter line has), and the
per-exec invocation flags - 'P', 'C' and 'O' equivalents plus a
transparent invocation mode as BPF_BINPRM_* bits - in the bprm.
Selection cannot go through bprm_change_interp() directly because
load_misc_binary() copies bprm->interp into argv[1] after the program
ran, hence the staging fields added in patch 1.

Registering (attaching) the struct_ops map publishes the handler under
its name in a registry keyed by the registering task's user namespace.
Activation reuses the existing text interface with a new 'B' type where
the interpreter field carries the handler name - it consistently names
whoever supplies the interpreter - and offset, magic, and mask must be
empty:

	echo ':origin:B::::nix:' > /proc/sys/fs/binfmt_misc/register

This keeps the existing permission and namespacing model completely
intact. Activating a handler requires the same write access to a
binfmt_misc instance as any other registration, a container mounting
its own instance escapes the host's entries exactly as before, and
shadowing e.g. all ELF binaries takes the same privilege as a static
'M' entry matching \x7fELF does today.

The only novelty is that matching becomes programmable. Handler lookup
walks the user namespace hierarchy upwards, mirroring how binfmt_misc
instances themselves are resolved, so a handler registered on the host
can be activated from a container's own instance without being forced
upon it.

The computed interpreter is opened with open_exec() under the caller's
credentials and goes through the full LSM vetting as the next binprm
level, exactly like a statically registered interpreter, so the program
cannot widen access. It only ever redirects the caller to something the
caller could exec anyway.

A 'B' entry carries no flags in the register string: the load program
chooses the invocation flags per exec through bpf_binprm_set_flags()
instead. BPF_BINPRM_PRESERVE_ARGV0, BPF_BINPRM_CREDENTIALS and
BPF_BINPRM_EXECFD keep the static 'P', 'C' and 'O' semantics -
BPF_BINPRM_CREDENTIALS honors the matched binary's suid bits exactly
as a static 'C' entry does, with the setuid transition gated by
vfsuid_has_mapping() in the caller's user namespace either way, which
makes 'B' handlers usable for a per-binary loader over setuid
binaries. BPF_BINPRM_TRANSPARENT has no static counterpart: the binary
is handed to the interpreter through AT_EXECFD with the argument
vector left untouched, so the exec looks like a direct execution of
the binary and a binary passed as an inaccessible O_CLOEXEC fd to
execveat() can be run at all. 'F' (pre-open a fixed interpreter) is
rejected: a 'B' entry has no fixed interpreter. AT_EXECVE_CHECK never
invokes programs and interpreter chains stay capped by the usual ELOOP
depth.

A handler for the Nix case then looks roughly like:

	SEC("struct_ops.s/match")
	bool BPF_PROG(nix_match, struct linux_binprm *bprm)
	{
		return !bpf_strncmp(bprm->buf, 4, "\x7f" "ELF");
	}

	SEC("struct_ops.s/load")
	int BPF_PROG(nix_load, struct linux_binprm *bprm)
	{
		char path[256];
		long n;

		n = bpf_path_d_path(&bprm->file->f_path, path, sizeof(path));
		if (n < 0)
			return n;

		/* derive the loader location from the binary's path */

		return bpf_binprm_set_interp(bprm, path, sizeof(path));
	}

	SEC(".struct_ops.link")
	struct binfmt_misc_ops nix = {
		.match = (void *)nix_match,
		.load = (void *)nix_load,
		.name = "nix",
	};

Farid, this should slot underneath your qemu demo from [4] with the
program ported to struct_ops. Feel free to take it from here.

[1]: https://lore.kernel.org/20260622043934.179879-1-farid.m.zakaria@gmail.com
[2]: https://lore.kernel.org/20260702214247.1253741-1-farid.m.zakaria@gmail.com
[3]: https://lore.kernel.org/20260703-meditation-ratsuchende-moratorium-9ecdf1f3f8bb@brauner
[4]: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Changes in v2:
- Rebase onto vfs-7.3.binfmt (write access fixes, RCU handler lookup
  and cleanups): the entry walk is lockless now.
- Split the handler into a match program consulted from the lookup
  walk itself - exactly like magic/extension matching - and the load
  program invoked once the walk has committed to the entry. Both are
  sleepable and can read the file: a match limited to the prefetched
  bprm->buf cannot recognise formats whose distinguishing data sits
  past 256 bytes, and with several handlers registered
  match-broadly-and-reject-in-load would break first-match-wins. The
  entry walk becomes an SRCU read-side section (srcu-fast) to allow
  that. The selection kfuncs are restricted to the load program by the
  verifier via the struct_ops member offset. A failing load fails the
  exec instead of falling through to later entries, so the walk is
  never left and re-entered: the v1 skip cursor and its rescan loop
  are gone and 'B' entries need no special semantics against
  concurrent (un)registration.
- Move the handler name from the offset field to the interpreter
  field: it consistently names whoever supplies the interpreter, a
  path for static entries and a handler for 'B' entries.
- Let the load program pass a single optional argument to the
  interpreter via the new bpf_binprm_set_interp_arg() kfunc, mirroring
  the optional argument of a #! interpreter line.
- Let the load program choose the invocation flags per exec via the
  new bpf_binprm_set_flags() kfunc - BPF_BINPRM_PRESERVE_ARGV0,
  BPF_BINPRM_CREDENTIALS and BPF_BINPRM_EXECFD mirroring 'P', 'C' and
  'O' - instead of fixing them at registration; the register string
  rejects 'P', 'C' and 'O' for 'B' entries. BPF_BINPRM_CREDENTIALS
  keeps the static 'C' semantics: the setuid transition stays gated by
  vfsuid_has_mapping() in the caller's user namespace.
- Add BPF_BINPRM_TRANSPARENT on top of the static flags: the binary is
  handed to the interpreter through AT_EXECFD with the argument vector
  left untouched, so the exec looks like a direct execution of the
  binary and a binary passed as an inaccessible O_CLOEXEC fd to
  execveat() can be run at all.
- Picked up Farid's selftests from his v2 posting and adapted them to
  the contract above: the register grammar, both programs sleepable
  with nix_origin's match reading PT_INTERP itself, the load return
  convention, and a skip on kernels without CONFIG_BINFMT_MISC_BPF.

---
Christian Brauner (8):
      exec: stash bpf-selected interpreter state in struct linux_binprm
      binfmt_misc: add binfmt_misc_ops bpf struct_ops
      binfmt_misc: let the entry lookup walk sleep
      binfmt_misc: wire up bpf-backed 'B' entries
      bpf: allow fs kfuncs for binfmt_misc_ops programs
      binfmt_misc: let bpf handlers pass an argument to the interpreter
      binfmt_misc: let a bpf handler choose the invocation flags per exec
      binfmt_misc: let a bpf handler run the interpreter transparently

Farid Zakaria (1):
      selftests/exec: add binfmt_misc bpf-backed handler test

 Documentation/admin-guide/binfmt-misc.rst        |  73 ++++-
 fs/Kconfig.binfmt                                |  14 +
 fs/Makefile                                      |   1 +
 fs/binfmt_misc.c                                 | 306 ++++++++++++++++----
 fs/binfmt_misc_bpf.c                             | 351 +++++++++++++++++++++++
 fs/bpf_fs_kfuncs.c                               |  23 +-
 fs/exec.c                                        |   2 +
 include/linux/binfmt_misc.h                      |  74 +++++
 include/linux/binfmts.h                          |   8 +
 tools/testing/selftests/exec/.gitignore          |   5 +
 tools/testing/selftests/exec/Makefile            |  48 ++++
 tools/testing/selftests/exec/binfmt_bpf_app.c    |  12 +
 tools/testing/selftests/exec/binfmt_bpf_interp.c |  15 +
 tools/testing/selftests/exec/binfmt_misc_bpf.c   | 277 ++++++++++++++++++
 tools/testing/selftests/exec/bpf_interp.bpf.c    |  61 ++++
 tools/testing/selftests/exec/nix_origin.bpf.c    | 224 +++++++++++++++
 16 files changed, 1434 insertions(+), 60 deletions(-)
---
base-commit: 79dec1889331ba2d4898886779f4d0a063f6a1ef
change-id: 20260707-work-bpf-binfmt_misc-e0b75341733e


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

* [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-15  0:28   ` Farid Zakaria
  2026-07-14 19:58 ` [PATCH v2 2/9] binfmt_misc: add binfmt_misc_ops bpf struct_ops Christian Brauner
                   ` (8 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

The upcoming bpf-backed binfmt_misc handlers decide how a binary is run
programmatically at exec time: the interpreter itself, an optional
single argument to pass to it, and the invocation flags that a static
binfmt_misc entry fixes at registration time. The selection runs before
load_misc_binary() has copied the binary path from bprm->interp into
the argument vector, so the selecting program cannot go through
bprm_change_interp() directly without clobbering argv[1].

Stage the selected state in the bprm instead, grouped in struct
binfmt_misc_bpf and embedded anonymously in struct linux_binprm so the
bprm->bpf_* accesses stay direct. The bprm is exclusively owned by the
task doing the exec so no synchronization is needed. The consumers
free and clear the fields once the exec attempt that set them is
finished; free_bprm() covers all error paths.

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

diff --git a/fs/exec.c b/fs/exec.c
index b92fe7db176c..c698aabe9abd 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1418,6 +1418,8 @@ static void free_bprm(struct linux_binprm *bprm)
 	/* If a binfmt changed the interp, free it. */
 	if (bprm->interp != bprm->filename)
 		kfree(bprm->interp);
+	kfree(bprm->bpf_interp);
+	kfree(bprm->bpf_interp_arg);
 	kfree(bprm->fdpath);
 	kfree(bprm);
 }
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index 7e7333b7bb0f..03e1794b5cbb 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -12,6 +12,13 @@ struct coredump_params;
 
 #define CORENAME_MAX_SIZE 128
 
+/* Interpreter selection staged by a bpf binfmt_misc handler. */
+struct binfmt_misc_bpf {
+	const char *bpf_interp;		/* interpreter selected by a bpf handler */
+	const char *bpf_interp_arg;	/* interpreter argument from a bpf handler */
+	u64 bpf_flags;			/* enum bpf_binprm_flags from a bpf handler */
+};
+
 /*
  * This structure is used to hold the arguments that are used when loading binaries.
  */
@@ -65,6 +72,7 @@ struct linux_binprm {
 				   of the time same as filename, but could be
 				   different for binfmt_{misc,script} */
 	const char *fdpath;	/* generated filename for execveat */
+	struct binfmt_misc_bpf;	/* bpf handler interpreter selection */
 	unsigned interp_flags;
 	int execfd;		/* File descriptor of the executable */
 	unsigned long exec;

-- 
2.53.0


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

* [PATCH v2 2/9] binfmt_misc: add binfmt_misc_ops bpf struct_ops
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 3/9] binfmt_misc: let the entry lookup walk sleep Christian Brauner
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

Add the bpf plumbing for binary type handlers whose matching and
interpreter selection are implemented by bpf programs instead of a
fixed magic/extension and a fixed interpreter string recorded at
registration time. This serves relocatable binary formats where the
interpreter must be computed per binary, e.g. relative to the location
of the binary itself, as discussed for hermetic Nix-style executables.

A handler is an instance of the new binfmt_misc_ops struct_ops with a
name that binfmt_misc entries reference it by and two ops:

        bool (*match)(struct linux_binprm *bprm);
        int (*load)(struct linux_binprm *bprm);

struct_ops is the sanctioned mechanism for this kind of user-supplied
policy callback: program types, attach types, and the uapi helper list
are frozen, and every recently added subsystem hook (bpf qdisc, SMC
handshake control, io_uring loop ops, sched_ext) is a struct_ops user.
The ops receive the bprm as a trusted BTF pointer, so a program can
match on the header in bprm->buf, read arbitrary file content via
bpf_dynptr_from_file() to parse e.g. ELF program headers, and inspect
the binary's location. No dedicated program type, ctx blob, or uapi
helper is needed.

The two ops split along what they decide, not what they may do: the
match program decides whether the handler applies to a binary, the
load program decides how a matched binary is run. Both are required
to be sleepable. Matching cannot be limited to the prefetched 256
bytes in bprm->buf: deciding whether a handler applies takes e.g.
parsing the ELF program headers to find an interpreter segment, which
sits at an arbitrary file offset, and non-sleepable file reads are
limited to whatever happens to be resident in the page cache. A match
program that cannot read the file reliably would have to match
broadly and leave the rejection to its load program, which breaks
first-match-wins entry semantics the moment more than one handler is
registered. Reliable file reads at exec time fault in the file's
pages, so both ops must be able to sleep. This also constrains the
caller: binfmt_misc must invoke both from sleepable context, which a
later patch takes care of. Both ops are required; a handler that
wants to decide everything from the load program supplies a match
program that just returns true.

The load program communicates its decisions through three new kfuncs:

        int bpf_binprm_set_interp(struct linux_binprm *bprm,
                                  const char *path, size_t path__sz);

selects the interpreter and enforces an absolute path shorter than
PATH_MAX.

        int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
                                      const char *arg, size_t arg__sz);

passes a single optional argument to the interpreter, mirroring the
optional argument of a #! interpreter line - something a static entry
cannot express at all.

        int bpf_binprm_set_flags(struct linux_binprm *bprm,
                                 enum bpf_binprm_flags flags);

chooses the invocation flags for this exec, with
BPF_BINPRM_PRESERVE_ARGV0, BPF_BINPRM_CREDENTIALS and
BPF_BINPRM_EXECFD mapping to 'P', 'C' and 'O'. Unknown bits are
rejected so a program built against a newer kernel fails loudly on an
older one rather than silently losing a flag. Repeated calls replace
the staged flags and a zero argument clears them again - the
set-or-clear semantics of bpf_bprm_opts_set() on the same struct. A
flags word carries this better than a kfunc per flag: it is one call,
it is set atomically, and new behaviour is a new bit rather than new
surface - the same shape the register string's flags field already
has.

All three stage their result in the bprm; consuming it from
load_misc_binary() is wired up by the following patches. The bprm is
exclusively owned by the task doing the exec, so no shared or per-CPU
state is involved and nothing here can race. The kfuncs are registered
for struct_ops programs with a filter that limits them to the load
program of a binfmt_misc_ops instance, keyed off the struct_ops member
offset the program attaches to: match decides whether a handler
applies, load decides how the binary is run, and the verifier enforces
that split at program load time.

Registering an ops instance (updating the struct_ops map or attaching
its link) publishes the handler under its name in a registry keyed by
the registering task's user namespace. Lookups walk the user namespace
hierarchy upwards, mirroring how binfmt_misc instances themselves are
resolved in current_binfmt_misc(). Consumers take a reference on the
ops via bpf_struct_ops_get() which pins the underlying map and
programs, so an activated handler keeps working even if the map is
deleted or the registering container goes away; deregistration only
prevents new activations, exactly like unregistering a tcp congestion
ops with live users.

Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/Kconfig.binfmt           |  14 ++
 fs/Makefile                 |   1 +
 fs/binfmt_misc_bpf.c        | 349 ++++++++++++++++++++++++++++++++++++++++++++
 include/linux/binfmt_misc.h |  71 +++++++++
 4 files changed, 435 insertions(+)

diff --git a/fs/Kconfig.binfmt b/fs/Kconfig.binfmt
index 1949e25c7741..daeac4889d03 100644
--- a/fs/Kconfig.binfmt
+++ b/fs/Kconfig.binfmt
@@ -168,6 +168,20 @@ config BINFMT_MISC
 	  you have use for it; the module is called binfmt_misc. If you
 	  don't know what to answer at this point, say Y.
 
+config BINFMT_MISC_BPF
+	bool "BPF-selected interpreters for misc binaries"
+	depends on BINFMT_MISC=y
+	depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
+	help
+	  Allow binfmt_misc binary type handlers to be implemented as bpf
+	  struct_ops programs. Instead of matching a fixed magic and
+	  redirecting to a fixed interpreter recorded at registration time
+	  such handlers match binaries programmatically and compute the
+	  interpreter to use per binary, e.g. relative to the location of
+	  the binary itself.
+
+	  If you don't know what to answer at this point, say N.
+
 config COREDUMP
 	bool "Enable core dump support" if EXPERT
 	default y
diff --git a/fs/Makefile b/fs/Makefile
index 89a8a9d207d1..499c6670f0c1 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -33,6 +33,7 @@ obj-$(CONFIG_FS_ENCRYPTION)	+= crypto/
 obj-$(CONFIG_FS_VERITY)		+= verity/
 obj-$(CONFIG_FILE_LOCKING)      += locks.o
 obj-$(CONFIG_BINFMT_MISC)	+= binfmt_misc.o
+obj-$(CONFIG_BINFMT_MISC_BPF)	+= binfmt_misc_bpf.o
 obj-$(CONFIG_BINFMT_SCRIPT)	+= binfmt_script.o
 obj-$(CONFIG_BINFMT_ELF)	+= binfmt_elf.o
 obj-$(CONFIG_COMPAT_BINFMT_ELF)	+= compat_binfmt_elf.o
diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c
new file mode 100644
index 000000000000..00c787e8bdcc
--- /dev/null
+++ b/fs/binfmt_misc_bpf.c
@@ -0,0 +1,349 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * BPF-backed binary type handlers for binfmt_misc.
+ *
+ * A handler is a struct binfmt_misc_ops struct_ops map. Loading and
+ * registering it makes the handler available under its name in the user
+ * namespace it was registered in. A binfmt_misc 'B' entry activates it:
+ *
+ *   echo ':entry:B::::<handler-name>:' > <binfmt_misc>/register
+ */
+
+#include <linux/binfmt_misc.h>
+#include <linux/binfmts.h>
+#include <linux/bpf.h>
+#include <linux/bpf_verifier.h>
+#include <linux/btf.h>
+#include <linux/btf_ids.h>
+#include <linux/cred.h>
+#include <linux/init.h>
+#include <linux/limits.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/user_namespace.h>
+
+struct bm_bpf_ops_reg {
+	struct list_head list;
+	const struct binfmt_misc_ops *ops;
+	struct bpf_link *link;
+	struct user_namespace *user_ns;
+};
+
+static DEFINE_SPINLOCK(bm_bpf_ops_lock);
+static LIST_HEAD(bm_bpf_ops_list);
+
+static struct bpf_struct_ops bpf_binfmt_misc_ops;
+
+static struct bm_bpf_ops_reg *bm_bpf_ops_find(const struct user_namespace *user_ns,
+					      const char *name)
+{
+	struct bm_bpf_ops_reg *reg;
+
+	lockdep_assert_held(&bm_bpf_ops_lock);
+
+	list_for_each_entry(reg, &bm_bpf_ops_list, list) {
+		if (reg->user_ns == user_ns && !strcmp(reg->ops->name, name))
+			return reg;
+	}
+	return NULL;
+}
+
+/**
+ * binfmt_misc_get_ops - look up a bpf binary type handler by name
+ * @user_ns: user namespace of the binfmt_misc instance
+ * @name: name the handler was registered under
+ *
+ * Search @user_ns and its ancestors for a handler named @name, mirroring
+ * the instance lookup in current_binfmt_misc(). The returned handler stays
+ * callable until binfmt_misc_put_ops() even if the backing struct_ops map
+ * is detached or deleted in the meantime.
+ *
+ * Return: the handler on success, NULL on failure
+ */
+const struct binfmt_misc_ops *binfmt_misc_get_ops(struct user_namespace *user_ns,
+						  const char *name)
+{
+	const struct user_namespace *ns;
+	struct bm_bpf_ops_reg *reg;
+
+	guard(spinlock)(&bm_bpf_ops_lock);
+
+	for (ns = user_ns; ns; ns = ns->parent) {
+		reg = bm_bpf_ops_find(ns, name);
+		if (!reg)
+			continue;
+		if (!bpf_struct_ops_get(reg->ops))
+			return NULL;
+		return reg->ops;
+	}
+	return NULL;
+}
+
+void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops)
+{
+	bpf_struct_ops_put(ops);
+}
+
+bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog)
+{
+	return prog->type == BPF_PROG_TYPE_STRUCT_OPS &&
+	       prog->aux->st_ops == &bpf_binfmt_misc_ops;
+}
+
+__bpf_kfunc_start_defs();
+
+/**
+ * bpf_binprm_set_interp - select the interpreter for the current exec
+ * @bprm: binary that is being executed
+ * @path: absolute path to the interpreter
+ * @path__sz: size of the @path buffer, including the terminating NUL
+ *
+ * To be called from the load program of a struct binfmt_misc_ops handler
+ * before returning zero; the verifier rejects the call from any other
+ * program, including the handler's own match program. The path is opened
+ * with the credentials of the task doing the exec after the program
+ * returns.
+ *
+ * Return: 0 on success, a negative errno on failure
+ */
+__bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm,
+				      const char *path, size_t path__sz)
+{
+	size_t len;
+	char *interp;
+
+	if (!path__sz)
+		return -EINVAL;
+	len = strnlen(path, path__sz);
+	if (len == path__sz)
+		return -EINVAL;
+	if (path[0] != '/')
+		return -EINVAL;
+	if (len >= PATH_MAX)
+		return -ENAMETOOLONG;
+
+	interp = kmemdup_nul(path, len, GFP_KERNEL);
+	if (!interp)
+		return -ENOMEM;
+
+	kfree(bprm->bpf_interp);
+	bprm->bpf_interp = interp;
+	return 0;
+}
+
+/**
+ * bpf_binprm_set_interp_arg - set a single argument for the interpreter
+ * @bprm: binary that is being executed
+ * @arg: argument to pass to the interpreter
+ * @arg__sz: size of the @arg buffer, including the terminating NUL
+ *
+ * To be called from the load program of a struct binfmt_misc_ops handler. The
+ * argument is passed to the interpreter ahead of the binary, mirroring the
+ * single optional argument of a #! interpreter line. Calling it again
+ * replaces the argument.
+ *
+ * Return: 0 on success, a negative errno on failure
+ */
+__bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
+					  const char *arg, size_t arg__sz)
+{
+	size_t len;
+	char *val;
+
+	if (!arg__sz)
+		return -EINVAL;
+	len = strnlen(arg, arg__sz);
+	if (len == arg__sz)
+		return -EINVAL;
+	if (!len)
+		return -EINVAL;
+
+	val = kmemdup_nul(arg, len, GFP_KERNEL);
+	if (!val)
+		return -ENOMEM;
+
+	kfree(bprm->bpf_interp_arg);
+	bprm->bpf_interp_arg = val;
+	return 0;
+}
+
+/**
+ * bpf_binprm_set_flags - choose the interpreter invocation flags for this exec
+ * @bprm: binary that is being executed
+ * @flags: an OR of enum bpf_binprm_flags values
+ *
+ * To be called from the load program of a struct binfmt_misc_ops handler. It
+ * decides per exec what a static entry fixes at registration with the P, C and
+ * 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.
+ *
+ * Return: 0 on success, -EINVAL if @flags contains an unknown bit
+ */
+__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))
+		return -EINVAL;
+
+	bprm->bpf_flags = flags;
+	return 0;
+}
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(bm_bpf_kfunc_ids)
+BTF_ID_FLAGS(func, bpf_binprm_set_interp, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_binprm_set_interp_arg, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_binprm_set_flags, KF_SLEEPABLE)
+BTF_KFUNCS_END(bm_bpf_kfunc_ids)
+
+static int bm_bpf_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id)
+{
+	if (!btf_id_set8_contains(&bm_bpf_kfunc_ids, kfunc_id))
+		return 0;
+	/* Only the load program decides how a binary is run. */
+	if (bpf_prog_is_binfmt_misc_ops(prog) &&
+	    prog->aux->attach_st_ops_member_off == offsetof(struct binfmt_misc_ops, load))
+		return 0;
+	return -EACCES;
+}
+
+static const struct btf_kfunc_id_set bm_bpf_kfunc_set = {
+	.owner	= THIS_MODULE,
+	.set	= &bm_bpf_kfunc_ids,
+	.filter	= bm_bpf_kfunc_filter,
+};
+
+static bool bm_bpf_ops__match(struct linux_binprm *bprm)
+{
+	return false;
+}
+
+static int bm_bpf_ops__load(struct linux_binprm *bprm)
+{
+	return 0;
+}
+
+static struct binfmt_misc_ops bm_bpf_ops_stubs = {
+	.match = bm_bpf_ops__match,
+	.load = bm_bpf_ops__load,
+};
+
+static int bm_bpf_init(struct btf *btf)
+{
+	return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
+					 &bm_bpf_kfunc_set);
+}
+
+static int bm_bpf_check_member(const struct btf_type *t,
+			       const struct btf_member *member,
+			       const struct bpf_prog *prog)
+{
+	u32 moff = __btf_member_bit_offset(t, member) / 8;
+
+	switch (moff) {
+	case offsetof(struct binfmt_misc_ops, match):
+	case offsetof(struct binfmt_misc_ops, load):
+		/* Reliable file reads at exec time require sleeping. */
+		if (!prog->sleepable)
+			return -EINVAL;
+		break;
+	}
+	return 0;
+}
+
+static int bm_bpf_init_member(const struct btf_type *t,
+			      const struct btf_member *member,
+			      void *kdata, const void *udata)
+{
+	const struct binfmt_misc_ops *uops = udata;
+	struct binfmt_misc_ops *ops = kdata;
+	u32 moff = __btf_member_bit_offset(t, member) / 8;
+
+	switch (moff) {
+	case offsetof(struct binfmt_misc_ops, name):
+		if (bpf_obj_name_cpy(ops->name, uops->name,
+				     sizeof(ops->name)) <= 0)
+			return -EINVAL;
+		return 1;
+	}
+	return 0;
+}
+
+static int bm_bpf_validate(void *kdata)
+{
+	struct binfmt_misc_ops *ops = kdata;
+
+	if (!ops->match || !ops->load)
+		return -EINVAL;
+	return 0;
+}
+
+static int bm_bpf_reg(void *kdata, struct bpf_link *link)
+{
+	struct binfmt_misc_ops *ops = kdata;
+	struct bm_bpf_ops_reg *reg;
+
+	reg = kzalloc_obj(*reg, GFP_KERNEL_ACCOUNT);
+	if (!reg)
+		return -ENOMEM;
+
+	reg->ops = ops;
+	reg->link = link;
+	reg->user_ns = get_user_ns(current_user_ns());
+
+	guard(spinlock)(&bm_bpf_ops_lock);
+
+	if (bm_bpf_ops_find(reg->user_ns, ops->name)) {
+		put_user_ns(reg->user_ns);
+		kfree(reg);
+		return -EEXIST;
+	}
+
+	list_add(&reg->list, &bm_bpf_ops_list);
+	return 0;
+}
+
+static void bm_bpf_unreg(void *kdata, struct bpf_link *link)
+{
+	struct bm_bpf_ops_reg *reg;
+
+	guard(spinlock)(&bm_bpf_ops_lock);
+
+	list_for_each_entry(reg, &bm_bpf_ops_list, list) {
+		if (reg->ops == kdata && reg->link == link) {
+			list_del(&reg->list);
+			put_user_ns(reg->user_ns);
+			kfree(reg);
+			return;
+		}
+	}
+}
+
+static const struct bpf_verifier_ops bm_bpf_verifier_ops = {
+	.get_func_proto		= bpf_base_func_proto,
+	.is_valid_access	= bpf_tracing_btf_ctx_access,
+};
+
+static struct bpf_struct_ops bpf_binfmt_misc_ops = {
+	.verifier_ops	= &bm_bpf_verifier_ops,
+	.init		= bm_bpf_init,
+	.check_member	= bm_bpf_check_member,
+	.init_member	= bm_bpf_init_member,
+	.validate	= bm_bpf_validate,
+	.reg		= bm_bpf_reg,
+	.unreg		= bm_bpf_unreg,
+	.cfi_stubs	= &bm_bpf_ops_stubs,
+	.name		= "binfmt_misc_ops",
+	.owner		= THIS_MODULE,
+};
+
+static int __init bm_bpf_struct_ops_init(void)
+{
+	return register_bpf_struct_ops(&bpf_binfmt_misc_ops, binfmt_misc_ops);
+}
+late_initcall(bm_bpf_struct_ops_init);
diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h
new file mode 100644
index 000000000000..d3112a00cc19
--- /dev/null
+++ b/include/linux/binfmt_misc.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_BINFMT_MISC_H
+#define _LINUX_BINFMT_MISC_H
+
+#include <linux/types.h>
+
+struct bpf_prog;
+struct linux_binprm;
+struct user_namespace;
+
+#define BINFMT_MISC_OPS_NAME_MAX 16
+
+/**
+ * enum bpf_binprm_flags - per-exec invocation flags a load program can request
+ * @BPF_BINPRM_PRESERVE_ARGV0: keep the caller's argv[0] (like the 'P' flag)
+ * @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)
+ *
+ * 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.
+ */
+enum bpf_binprm_flags {
+	BPF_BINPRM_PRESERVE_ARGV0	= (1ULL << 0),
+	BPF_BINPRM_CREDENTIALS		= (1ULL << 1),
+	BPF_BINPRM_EXECFD		= (1ULL << 2),
+};
+
+/**
+ * struct binfmt_misc_ops - bpf-backed binary type handler
+ * @match: decide whether the handler applies to @bprm; consulted from the
+ *         entry lookup walk like static magic and extension matching, in
+ *         registration order with first-match-wins semantics; sleepable,
+ *         so it can read the binary to decide, but the verifier rejects
+ *         the interpreter selection kfuncs in it
+ * @load:  select an interpreter for the matched @bprm via
+ *         bpf_binprm_set_interp() and return zero; a match is committed, so
+ *         a failure fails the exec instead of falling through to later
+ *         entries; -ENOEXEC does not fail the exec but moves on to the
+ *         remaining binary formats
+ * @name: name that 'B' entries reference the handler by
+ */
+struct binfmt_misc_ops {
+	bool (*match)(struct linux_binprm *bprm);
+	int (*load)(struct linux_binprm *bprm);
+	char name[BINFMT_MISC_OPS_NAME_MAX];
+};
+
+#ifdef CONFIG_BINFMT_MISC_BPF
+const struct binfmt_misc_ops *binfmt_misc_get_ops(struct user_namespace *user_ns,
+						  const char *name);
+void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops);
+bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog);
+#else
+static inline const struct binfmt_misc_ops *
+binfmt_misc_get_ops(struct user_namespace *user_ns, const char *name)
+{
+	return NULL;
+}
+
+static inline void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops)
+{
+}
+
+static inline bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog)
+{
+	return false;
+}
+#endif /* CONFIG_BINFMT_MISC_BPF */
+
+#endif /* _LINUX_BINFMT_MISC_H */

-- 
2.53.0


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

* [PATCH v2 3/9] binfmt_misc: let the entry lookup walk sleep
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 2/9] binfmt_misc: add binfmt_misc_ops bpf struct_ops Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries Christian Brauner
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

The upcoming bpf-backed binary type handlers run a match program from
the entry lookup walk in load_misc_binary(). Deciding whether a handler
applies means reading the binary - parsing ELF program headers sitting
at arbitrary file offsets, say - and reliable file reads at exec time
fault in the file's pages, so the walk must tolerate an entry's
evaluation sleeping.

Switch the walk from RCU to SRCU in its fast flavor: srcu-fast read
sections may block while the read side stays practically as cheap as
the RCU read lock it replaces, so the common static-entry lookup does
not pay for the new capability. Entry freeing moves from kfree_rcu()
to call_srcu(). Removal still unlinks the entry immediately and never
blocks: a walker sleeping inside an entry's evaluation just keeps the
entry alive until it leaves the read section. The module exit path
flushes pending callbacks with srcu_barrier().

Take the reference on a matched entry at the match point inside the
walk instead of retrying the whole search when the refcount raise
fails. A restarted search was harmless when an entry's evaluation was
a memcmp() on bprm->buf, but re-running match programs that may sleep
on entries that were already consulted is not. An entry whose refcount
hit zero is unlinked and dying, so treating it as absent and walking
on is exactly what the bounded retry loop converged to, without ever
evaluating an entry twice.

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

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index fcaad14f81a3..f7264b13645d 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -29,6 +29,7 @@
 #include <linux/refcount.h>
 #include <linux/seq_file.h>
 #include <linux/slab.h>
+#include <linux/srcu.h>
 #include <linux/string.h>
 #include <linux/string_helpers.h>
 #include <linux/uaccess.h>
@@ -82,6 +83,9 @@ struct binfmt_misc_entry {
 /* Trailing delimiter pad so field parsing always terminates at a delimiter. */
 #define MISC_DELIM_PAD 8
 
+/* Protects the entry walk in load_misc_binary(), which may sleep in it. */
+DEFINE_STATIC_SRCU_FAST(bm_entries_srcu);
+
 /* Check if @e's magic matches @bprm's buffer, applying the mask if set. */
 static bool entry_matches_magic(const struct binfmt_misc_entry *e,
 				const struct linux_binprm *bprm)
@@ -111,11 +115,14 @@ static bool entry_matches_extension(const struct binfmt_misc_entry *e,
  * @bprm: binary for which we are looking for a handler
  *
  * Search for a binary type handler for @bprm in the list of registered binary
- * type handlers.
+ * type handlers. The matched entry is returned with a reference taken while
+ * the walk still held it; a dying entry - unlinked with its last reference
+ * gone - cannot be matched and the walk moves on.
  *
- * The caller must hold the RCU read lock.
+ * The caller must hold the bm_entries_srcu read lock, which allows an
+ * entry's evaluation to sleep.
  *
- * Return: binary type list entry on success, NULL on failure
+ * Return: referenced binary type list entry on success, NULL on failure
  */
 static struct binfmt_misc_entry *
 search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm)
@@ -125,18 +132,23 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm)
 	struct binfmt_misc_entry *e;
 
 	/* Walk all the registered handlers. */
-	hlist_for_each_entry_rcu(e, &misc->entries, node) {
+	hlist_for_each_entry_rcu(e, &misc->entries, node,
+				 srcu_read_lock_held(&bm_entries_srcu)) {
 		/* Make sure this one is currently enabled. */
 		if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags))
 			continue;
 
 		if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) {
-			if (entry_matches_magic(e, bprm))
-				return e;
+			if (!entry_matches_magic(e, bprm))
+				continue;
 		} else {
-			if (entry_matches_extension(e, ext))
-				return e;
+			if (!entry_matches_extension(e, ext))
+				continue;
 		}
+
+		/* A dying entry cannot be matched, walk on. */
+		if (refcount_inc_not_zero(&e->users))
+			return e;
 	}
 
 	return NULL;
@@ -147,24 +159,22 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm)
  * @misc: handle to binfmt_misc instance
  * @bprm: binary for which we are looking for a handler
  *
- * Try to find a binfmt handler for the binary type. If one is found take a
- * reference to protect against removal via bm_{entry,status}_write(). The
- * refcount of an entry can only drop to zero once it has been unlinked and
- * a restarted search cannot find an unlinked entry again so the retry loop
- * is bounded.
+ * Try to find a binfmt handler for the binary type. If one is found it is
+ * returned with a reference protecting it against removal via
+ * bm_{entry,status}_write().
  *
  * Return: binary type list entry on success, NULL on failure
  */
 static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc,
 						    struct linux_binprm *bprm)
 {
-	struct binfmt_misc_entry *e;
+	guard(srcu_fast)(&bm_entries_srcu);
+	return search_binfmt_handler(misc, bprm);
+}
 
-	guard(rcu)();
-	do {
-		e = search_binfmt_handler(misc, bprm);
-	} while (e && !refcount_inc_not_zero(&e->users));
-	return e;
+static void bm_entry_free_rcu(struct rcu_head *rcu)
+{
+	kfree(container_of(rcu, struct binfmt_misc_entry, rcu));
 }
 
 /**
@@ -182,8 +192,8 @@ static void put_binfmt_handler(struct binfmt_misc_entry *e)
 			exe_file_allow_write_access(e->interp_file);
 			filp_close(e->interp_file, NULL);
 		}
-		/* Lockless walkers may still dereference this entry. */
-		kfree_rcu(e, rcu);
+		/* Walkers may still dereference this entry, even sleeping. */
+		call_srcu(&bm_entries_srcu, &e->rcu, bm_entry_free_rcu);
 	}
 }
 
@@ -670,7 +680,7 @@ static void bm_evict_inode(struct inode *inode)
  * Adding and removing entries via bm_{entry,register,status}_write() and
  * unlink(2) happens under the exclusively held inode lock of the root
  * dentry keeping the list stable for writers. load_misc_binary() walks it
- * concurrently under RCU. The entries_lock is only held around the actual
+ * concurrently under SRCU. The entries_lock is only held around the actual
  * unlink to serialize against bm_evict_inode() which unlinks entries
  * during umount without holding the root inode lock.
  */
@@ -1054,6 +1064,8 @@ static void __exit exit_misc_binfmt(void)
 {
 	unregister_binfmt(&misc_format);
 	unregister_filesystem(&bm_fs_type);
+	/* Flush pending bm_entry_free_rcu() callbacks before the text goes. */
+	srcu_barrier(&bm_entries_srcu);
 }
 
 core_initcall(init_misc_binfmt);

-- 
2.53.0


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

* [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (2 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 3/9] binfmt_misc: let the entry lookup walk sleep Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-15  0:28   ` Farid Zakaria
  2026-07-14 19:58 ` [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs Christian Brauner
                   ` (5 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

Activate a registered binfmt_misc_ops handler through the existing text
interface with the new 'B' entry type:

        echo ':name:B::::<handler-name>:' > <binfmt_misc>/register

The offset, magic, and mask fields must be empty since the program does
the matching; the interpreter field carries the handler name since the
program supplies the interpreter. Reusing the register file keeps the
existing permission model intact: activating a handler requires the
same write access to a binfmt_misc instance as any other registration,
and the per user namespace instance semantics apply unchanged. A 'B'
entry in a container's own instance shadows the host's handlers just
like any other entry, and the privilege needed to shadow e.g. all ELF
binaries is the same as for a static 'M' entry matching \x7fELF today;
the only novelty is that matching becomes programmable.

The entry takes its own reference on the ops for its whole lifetime.
It is dropped from the SRCU callback that frees the entry rather than
synchronously on the final put: a walker may be asleep inside the
handler's match program while the entry's last reference goes away, so
the ops must stay callable until every walker has left the read
section - the same deferral the entry's own memory already gets. The
registration failure path, where the users refcount is not live yet,
drops it explicitly.

The match program runs from the lookup walk like magic and extension
matching and under the same rules: strict registration order, first
match wins. The walk became an SRCU read-side section in the previous
patch, so the program can sleep: it decides on the actual file
content - program headers beyond the prefetched bprm->buf, say - not
just on whatever happens to be resident in the page cache. A match
commits the exec to the handler. The sleepable load program then
selects the interpreter from load_misc_binary() by calling
bpf_binprm_set_interp() and returning zero; a failure fails the exec
instead of falling through to later entries. The walk is never left
and re-entered, so 'B' entries need no special semantics against
concurrent registration and removal whatsoever. -ENOEXEC keeps its
usual meaning and moves on to the remaining binary formats - a handler
whose load program discovers that it cannot serve the binary after all
hands it back to them - and so does returning zero without having
selected an interpreter; other program-supplied errors are clamped to
the errno range.

The 'F' flag is rejected for 'B' entries: it exists to pre-open a fixed
interpreter at registration time in the registrar's context, and a 'B'
entry has no fixed interpreter to pre-open.

'C' is accepted and behaves exactly as it does for a static entry. It
honors the suid bits of the matched binary while executing the
interpreter, which makes 'B' handlers usable for the setuid case, e.g.
a per-binary loader. This does not let the program's registrant widen
access: bprm_fill_uid() gates the credential transition on
vfsuid_has_mapping() in the caller's user namespace, so the interpreter
can only ever run as a uid that is mapped there, identical to a static
'C' entry. The computed path is opened with open_exec() under the
caller's credentials with the usual LSM and noexec checks, and the
programs run before the transition with the caller's credentials,
never elevated.

Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst |  47 +++++++++-
 fs/binfmt_misc.c                          | 148 +++++++++++++++++++++++++++---
 2 files changed, 180 insertions(+), 15 deletions(-)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 306ef48f5de6..45541604d528 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -26,7 +26,8 @@ Here is what the fields mean:
    name below ``/proc/sys/fs/binfmt_misc``; cannot contain slashes ``/`` for
    obvious reasons.
 - ``type``
-   is the type of recognition. Give ``M`` for magic and ``E`` for extension.
+   is the type of recognition. Give ``M`` for magic, ``E`` for extension and
+   ``B`` for a bpf-backed handler (see below).
 - ``offset``
    is the offset of the magic/mask in the file, counted in bytes. This
    defaults to 0 if you omit it (i.e. you write ``:name:type::magic...``).
@@ -48,7 +49,8 @@ Here is what the fields mean:
    filename extension matching.
 - ``interpreter``
    is the program that should be invoked with the binary as first
-   argument (specify the full path)
+   argument (specify the full path). For ``B`` entries this field
+   carries the name of the bpf handler instead (see below).
 - ``flags``
    is an optional field that controls several aspects of the invocation
    of the interpreter. It is a string of capital letters, each controls a
@@ -97,6 +99,47 @@ There are some restrictions:
    offset+size(magic) has to be less than 128
  - the interpreter string may not exceed 127 characters
 
+
+bpf-backed handlers
+-------------------
+
+With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter
+selection can be delegated to bpf programs. A handler is an instance of the
+``binfmt_misc_ops`` struct_ops with a ``match`` and a ``load`` program and a
+``name``. Once the struct_ops map is registered the handler can be activated
+with a ``B`` entry that references it by name in the ``interpreter`` field
+and carries neither offset, magic, nor mask::
+
+    echo ':qemu:B::::my_handler:' > register
+
+Both programs receive the ``linux_binprm`` of the binary and both can
+sleep. The ``match`` program decides whether the handler applies: it is
+consulted during the entry walk exactly like magic and extension matching,
+in the same registration order with the same first-match-wins semantics.
+Unlike static matching it is not limited to the prefetched first bytes of
+the file in ``bprm->buf``: it can read the file, e.g. to parse ELF program
+headers whose data sits at arbitrary offsets. It only decides, though: the
+selection kfuncs below are rejected in it. The ``load`` program of the
+matched handler then selects the interpreter: it can equally read the file
+and derive the interpreter from the binary's location. It selects the
+interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an
+absolute path and returning ``0``. A match is committed: a failing
+``load`` fails the exec with its error instead of falling through to later
+entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The
+interpreter is opened with the credentials of the task doing the exec,
+exactly as a statically registered interpreter would be.
+
+Handlers are looked up in the user namespace the struct_ops map was
+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.
+
+The ``F`` flag cannot be combined with ``B`` entries: it pre-opens a fixed
+interpreter at registration time and a ``B`` entry has none. The ``C`` flag
+works as it does for a static entry: the interpreter runs with the matched
+binary's credentials, bounded to user namespaces that map the binary's owner
+just like any other setuid exec.
+
 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
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index f7264b13645d..a8f641c4ec2b 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -10,6 +10,7 @@
 
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
+#include <linux/binfmt_misc.h>
 #include <linux/binfmts.h>
 #include <linux/bitops.h>
 #include <linux/bits.h>
@@ -39,6 +40,7 @@
 enum binfmt_misc_entry_bits {
 	MISC_FMT_ENABLED_BIT	= 0,
 	MISC_FMT_MAGIC_BIT	= 1,
+	MISC_FMT_BPF_BIT	= 2,
 };
 
 /* Entry behavior flags, fixed at registration time. */
@@ -60,6 +62,8 @@ struct binfmt_misc_entry {
 	char *name;
 	struct dentry *dentry;
 	struct file *interp_file;
+	const struct binfmt_misc_ops *bpf_ops;	/* bpf-backed handler ('B') */
+	const char *bpf_ops_name;
 	refcount_t users;		/* sync removal with load_misc_binary() */
 	struct rcu_head rcu;
 	char buf[];			/* register string, fields point in here */
@@ -115,9 +119,11 @@ static bool entry_matches_extension(const struct binfmt_misc_entry *e,
  * @bprm: binary for which we are looking for a handler
  *
  * Search for a binary type handler for @bprm in the list of registered binary
- * type handlers. The matched entry is returned with a reference taken while
- * the walk still held it; a dying entry - unlinked with its last reference
- * gone - cannot be matched and the walk moves on.
+ * type handlers. A 'B' entry's match program decides whether the handler
+ * applies; it may sleep to read the binary. The matched entry is returned
+ * with a reference taken while the walk still held it; a dying entry -
+ * unlinked with its last reference gone - cannot be matched and the walk
+ * moves on.
  *
  * The caller must hold the bm_entries_srcu read lock, which allows an
  * entry's evaluation to sleep.
@@ -138,7 +144,10 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm)
 		if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags))
 			continue;
 
-		if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) {
+		if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
+			if (!e->bpf_ops->match(bprm))
+				continue;
+		} else if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) {
 			if (!entry_matches_magic(e, bprm))
 				continue;
 		} else {
@@ -174,7 +183,12 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc,
 
 static void bm_entry_free_rcu(struct rcu_head *rcu)
 {
-	kfree(container_of(rcu, struct binfmt_misc_entry, rcu));
+	struct binfmt_misc_entry *e = container_of(rcu, struct binfmt_misc_entry, rcu);
+
+	/* No walker that could sleep in the handler's programs is left. */
+	if (e->bpf_ops)
+		binfmt_misc_put_ops(e->bpf_ops);
+	kfree(e);
 }
 
 /**
@@ -226,12 +240,51 @@ static struct binfmt_misc *current_binfmt_misc(void)
 	return &init_binfmt_misc;
 }
 
+/**
+ * entry_select_interpreter - get the interpreter for the matched @e
+ * @e: matched binary type handler
+ * @bprm: binary that is being executed
+ *
+ * A static entry carries its interpreter path, for a 'B' entry the
+ * handler's load program selects it. The match is committed, so a failing
+ * program fails the exec.
+ *
+ * Return: the interpreter on success, an ERR_PTR on failure
+ */
+static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
+					    struct linux_binprm *bprm)
+{
+	int retval;
+
+	if (!test_bit(MISC_FMT_BPF_BIT, &e->flags))
+		return e->interpreter;
+
+	/* Drop any interpreter a previous chain level staged. */
+	kfree(bprm->bpf_interp);
+	bprm->bpf_interp = NULL;
+
+	retval = e->bpf_ops->load(bprm);
+	if (retval) {
+		/* Keep a program-supplied error within errno range. */
+		if (retval > 0 || retval < -MAX_ERRNO)
+			retval = -ENOEXEC;
+		return ERR_PTR(retval);
+	}
+
+	/* Selecting an interpreter is part of the contract. */
+	if (!bprm->bpf_interp)
+		return ERR_PTR(-ENOEXEC);
+
+	return bprm->bpf_interp;
+}
+
 /*
  * 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;
 	int retval;
@@ -248,6 +301,10 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
 		return -ENOENT;
 
+	interpreter = entry_select_interpreter(fmt, bprm);
+	if (IS_ERR(interpreter))
+		return PTR_ERR(interpreter);
+
 	if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) {
 		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
 	} else {
@@ -266,13 +323,13 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	bprm->argc++;
 
 	/* add the interp as argv[0] */
-	retval = copy_string_kernel(fmt->interpreter, bprm);
+	retval = copy_string_kernel(interpreter, bprm);
 	if (retval < 0)
 		return retval;
 	bprm->argc++;
 
 	/* Update interp in case binfmt_script needs it. */
-	retval = bprm_change_interp(fmt->interpreter, bprm);
+	retval = bprm_change_interp(interpreter, bprm);
 	if (retval < 0)
 		return retval;
 
@@ -287,7 +344,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 			}
 		}
 	} else {
-		interp_file = open_exec(fmt->interpreter);
+		interp_file = open_exec(interpreter);
 	}
 	if (IS_ERR(interp_file))
 		return PTR_ERR(interp_file);
@@ -438,6 +495,27 @@ static char *parse_extension_fields(struct binfmt_misc_entry *e, char *p,
 	return p;
 }
 
+/*
+ * Parse the fields of a 'B' entry: the 'offset', 'magic' and 'mask' fields
+ * must be empty. The handler name is carried in the 'interpreter' field.
+ */
+static char *parse_bpf_fields(struct binfmt_misc_entry *e, char *p, char del)
+{
+	/* The 'offset' field must be empty. */
+	if (*p++ != del)
+		return NULL;
+
+	/* The 'magic' field must be empty. */
+	if (*p++ != del)
+		return NULL;
+
+	/* The 'mask' field must be empty. */
+	if (*p++ != del)
+		return NULL;
+
+	return p;
+}
+
 /*
  * This registers a new binary format, it recognises the syntax
  * ':name:type:offset:magic:mask:interpreter:flags'
@@ -502,13 +580,21 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 		pr_debug("register: type: M (magic)\n");
 		e->flags = BIT(MISC_FMT_ENABLED_BIT) | BIT(MISC_FMT_MAGIC_BIT);
 		break;
+	case 'B':
+		pr_debug("register: type: B (bpf)\n");
+		if (!IS_ENABLED(CONFIG_BINFMT_MISC_BPF))
+			return ERR_PTR(-EINVAL);
+		e->flags = BIT(MISC_FMT_ENABLED_BIT) | BIT(MISC_FMT_BPF_BIT);
+		break;
 	default:
 		return ERR_PTR(-EINVAL);
 	}
 	if (*p++ != del)
 		return ERR_PTR(-EINVAL);
 
-	if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags))
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags))
+		p = parse_bpf_fields(e, p, del);
+	else if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags))
 		p = parse_magic_fields(e, p, del);
 	else
 		p = parse_extension_fields(e, p, del);
@@ -521,9 +607,18 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 	if (!p)
 		return ERR_PTR(-EINVAL);
 	*p++ = '\0';
-	if (!e->interpreter[0])
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
+		/* The 'interpreter' field carries the handler name. */
+		e->bpf_ops_name = e->interpreter;
+		e->interpreter = NULL;
+		if (!e->bpf_ops_name[0])
+			return ERR_PTR(-EINVAL);
+		pr_debug("register: bpf handler: {%s}\n", e->bpf_ops_name);
+	} else if (!e->interpreter[0]) {
 		return ERR_PTR(-EINVAL);
-	pr_debug("register: interpreter: {%s}\n", e->interpreter);
+	} else {
+		pr_debug("register: interpreter: {%s}\n", e->interpreter);
+	}
 
 	/* Parse the 'flags' field. */
 	p = check_special_flags(p, e);
@@ -532,6 +627,17 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 	if (p != buf + count)
 		return ERR_PTR(-EINVAL);
 
+	/*
+	 * 'F' pre-opens a fixed interpreter at registration time which is
+	 * meaningless for a per-exec computed path. 'C' is fine: it honors the
+	 * suid bits of the matched binary exactly like a static entry, gated by
+	 * the same vfsuid_has_mapping() check in bprm_fill_uid() that keeps the
+	 * transition to uids mapped in the caller's user namespace.
+	 */
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
+	    (e->flags & MISC_FMT_OPEN_FILE))
+		return ERR_PTR(-EINVAL);
+
 	return no_free_ptr(e);
 }
 
@@ -585,7 +691,10 @@ static int bm_entry_show(struct seq_file *m, void *unused)
 	else
 		seq_puts(m, "disabled\n");
 
-	seq_printf(m, "interpreter %s\n", e->interpreter);
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags))
+		seq_printf(m, "bpf %s\n", e->bpf_ops->name);
+	else
+		seq_printf(m, "interpreter %s\n", e->interpreter);
 
 	/* print the special flags */
 	seq_puts(m, "flags: ");
@@ -599,7 +708,9 @@ static int bm_entry_show(struct seq_file *m, void *unused)
 		seq_putc(m, 'F');
 	seq_putc(m, '\n');
 
-	if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) {
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
+		/* The program does the matching. */
+	} else if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) {
 		seq_printf(m, "extension .%s\n", e->magic);
 	} else {
 		seq_printf(m, "offset %i\nmagic ", e->offset);
@@ -850,6 +961,15 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer,
 	if (IS_ERR(e))
 		return PTR_ERR(e);
 
+	if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
+		e->bpf_ops = binfmt_misc_get_ops(sb->s_user_ns, e->bpf_ops_name);
+		if (!e->bpf_ops) {
+			pr_notice("register: no bpf handler named %s\n",
+				  e->bpf_ops_name);
+			return -ENOENT;
+		}
+	}
+
 	if (e->flags & MISC_FMT_OPEN_FILE) {
 		/*
 		 * Now that we support unprivileged binfmt_misc mounts make
@@ -874,6 +994,8 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer,
 			exe_file_allow_write_access(f);
 			filp_close(f, NULL);
 		}
+		if (e->bpf_ops)
+			binfmt_misc_put_ops(e->bpf_ops);
 		return err;
 	}
 

-- 
2.53.0


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

* [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (3 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-15  0:28   ` Farid Zakaria
  2026-07-14 19:58 ` [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter Christian Brauner
                   ` (4 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

The fs kfuncs are currently exclusive to LSM programs. A binfmt_misc
handler needs a subset of them to do anything interesting: computing an
interpreter relative to the binary's location wants bpf_path_d_path()
on bprm->file->f_path from the load program, and matching on per-binary
metadata wants bpf_get_file_xattr() and friends right from the match
program.

Register the fs kfunc set for struct_ops programs as well and extend
the filter to admit binfmt_misc_ops programs. The xattr setters stay
exclusive to LSM programs: a binary type handler decides how to run a
binary, it has no business modifying filesystem state.

This only takes effect in builds that have the fs kfunc set at all,
i.e. CONFIG_BPF_LSM. Without it a binfmt_misc handler is limited to
bprm fields and the file-backed dynptr, which are provided by the
common kfunc set.

Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/bpf_fs_kfuncs.c | 23 ++++++++++++++++++++---
 1 file changed, 20 insertions(+), 3 deletions(-)

diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
index 768aca2dc0f0..aa1fe988b6d2 100644
--- a/fs/bpf_fs_kfuncs.c
+++ b/fs/bpf_fs_kfuncs.c
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0
 /* Copyright (c) 2024 Google LLC. */
 
+#include <linux/binfmt_misc.h>
 #include <linux/bpf.h>
 #include <linux/bpf_lsm.h>
 #include <linux/btf.h>
@@ -387,10 +388,20 @@ BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_real_inode, KF_SLEEPABLE | KF_RET_NULL)
 BTF_KFUNCS_END(bpf_fs_kfunc_set_ids)
 
+/* Side-effecting kfuncs that stay exclusive to LSM programs. */
+BTF_SET_START(bpf_fs_kfunc_lsm_only_ids)
+BTF_ID(func, bpf_set_dentry_xattr)
+BTF_ID(func, bpf_remove_dentry_xattr)
+BTF_SET_END(bpf_fs_kfunc_lsm_only_ids)
+
 static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
 {
-	if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id) ||
-	    prog->type == BPF_PROG_TYPE_LSM)
+	if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id))
+		return 0;
+	if (prog->type == BPF_PROG_TYPE_LSM)
+		return 0;
+	if (bpf_prog_is_binfmt_misc_ops(prog) &&
+	    !btf_id_set_contains(&bpf_fs_kfunc_lsm_only_ids, kfunc_id))
 		return 0;
 	return -EACCES;
 }
@@ -433,7 +444,13 @@ static const struct btf_kfunc_id_set bpf_fs_kfunc_set = {
 
 static int __init bpf_fs_kfuncs_init(void)
 {
-	return register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_fs_kfunc_set);
+	int ret;
+
+	ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_fs_kfunc_set);
+	if (ret || !IS_ENABLED(CONFIG_BINFMT_MISC_BPF))
+		return ret;
+	return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
+					 &bpf_fs_kfunc_set);
 }
 
 late_initcall(bpf_fs_kfuncs_init);

-- 
2.53.0


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

* [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (4 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-15  0:28   ` Farid Zakaria
  2026-07-14 19:58 ` [PATCH v2 7/9] binfmt_misc: let a bpf handler choose the invocation flags per exec Christian Brauner
                   ` (3 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

A bpf binfmt_misc handler selects an interpreter but, unlike binfmt_script,
load_misc_binary() builds the argument vector as just [interpreter, binary,
...] with no slot for an argument to the interpreter. A handler that wants
to reproduce a #! line therefore cannot express its single optional
argument, e.g. a handler that resolves $ORIGIN in a script's #! path loses
the argument that followed the interpreter.

Have load_misc_binary() consume the argument staged through the
bpf_binprm_set_interp_arg() kfunc and insert it between the interpreter and
the binary - the same position and single-argument semantics binfmt_script
gives the argument of a #! line. The argument is cleared once spliced into
the argument vector, and a load program that fails after staging one has it
dropped on the way out: whether the exec fails or -ENOEXEC hands the binary
back to the remaining formats, a stale argument cannot leak into a nested
interpreter's argv. This also lets static-style handlers pass a fixed
interpreter argument, which plain binfmt_misc has never been able to
express.

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

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 45541604d528..6128057160e3 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -129,6 +129,12 @@ entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The
 interpreter is opened with the credentials of the task doing the exec,
 exactly as a statically registered interpreter would be.
 
+The ``load`` program can also pass a single argument to the interpreter with
+the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the
+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.
+
 Handlers are looked up in the user namespace the struct_ops map was
 registered in, falling back to ancestor namespaces, mirroring how
 binfmt_misc instances themselves are looked up. The entry keeps the handler
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index a8f641c4ec2b..3fbc51197c76 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -268,14 +268,22 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
 		/* Keep a program-supplied error within errno range. */
 		if (retval > 0 || retval < -MAX_ERRNO)
 			retval = -ENOEXEC;
-		return ERR_PTR(retval);
+		goto drop_staged;
 	}
 
 	/* Selecting an interpreter is part of the contract. */
-	if (!bprm->bpf_interp)
-		return ERR_PTR(-ENOEXEC);
+	if (!bprm->bpf_interp) {
+		retval = -ENOEXEC;
+		goto drop_staged;
+	}
 
 	return bprm->bpf_interp;
+
+drop_staged:
+	/* A failing load leaves nothing behind for later entries. */
+	kfree(bprm->bpf_interp_arg);
+	bprm->bpf_interp_arg = NULL;
+	return ERR_PTR(retval);
 }
 
 /*
@@ -316,12 +324,26 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (fmt->flags & MISC_FMT_OPEN_BINARY)
 		bprm->have_execfd = 1;
 
-	/* make argv[1] be the path to the binary */
+	/* make the binary the last argument to the interpreter */
 	retval = copy_string_kernel(bprm->interp, bprm);
 	if (retval < 0)
 		return retval;
 	bprm->argc++;
 
+	/*
+	 * A single optional argument to the interpreter, inserted between it
+	 * and the binary just like the argument of a #! interpreter line.
+	 */
+	if (bprm->bpf_interp_arg) {
+		retval = copy_string_kernel(bprm->bpf_interp_arg, bprm);
+		if (retval < 0)
+			return retval;
+		bprm->argc++;
+		/* Consumed - don't let it leak into a nested interpreter's argv. */
+		kfree(bprm->bpf_interp_arg);
+		bprm->bpf_interp_arg = NULL;
+	}
+
 	/* add the interp as argv[0] */
 	retval = copy_string_kernel(interpreter, bprm);
 	if (retval < 0)

-- 
2.53.0


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

* [PATCH v2 7/9] binfmt_misc: let a bpf handler choose the invocation flags per exec
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (5 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 8/9] binfmt_misc: let a bpf handler run the interpreter transparently Christian Brauner
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

The 'P', 'C' and 'O' flags of a binfmt_misc entry - preserve argv[0],
compute credentials from the binary, and pass the binary as an open file
descriptor - are fixed at registration and apply to every binary the entry
matches. A bpf handler matches, selects the interpreter and reads the
binary per exec, so the flags should be its per-exec decision too: one
handler may match both setuid and non-setuid binaries, argv[0]-sensitive
ones and not.

Honor the flags the load program stages in bprm->bpf_flags through the
bpf_binprm_set_flags() kfunc: BPF_BINPRM_PRESERVE_ARGV0,
BPF_BINPRM_CREDENTIALS and BPF_BINPRM_EXECFD map to 'P', 'C' and 'O' and
keep the semantics of their static counterparts, credentials implying the
open file descriptor included.

Flags staged by a load program that then fails are dropped on the way out
so they cannot leak into a later handler's exec, and the argv[0] decision
acts on the entry's own choice instead of testing the accumulated
bprm->interp_flags bit, which an earlier chain level may have left set and
binfmt_misc never clears.

Since a 'B' entry's flags come from the program, it carries none in the
register string: 'P', 'C' and 'O' are rejected there alongside 'F', which
was already meaningless for it. load_misc_binary() takes the flags from
the entry for a static handler and from bprm->bpf_flags for a bpf one.

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

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 6128057160e3..7f42abf9cfba 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -135,17 +135,28 @@ 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``
+program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can
+decide them differently for each binary it handles:
+
+- ``BPF_BINPRM_PRESERVE_ARGV0`` keeps the caller's ``argv[0]`` (the ``P``
+  flag).
+- ``BPF_BINPRM_CREDENTIALS`` computes credentials from the binary (the ``C``
+  flag), bounded to user namespaces that map the binary's owner just like
+  any other setuid exec.
+- ``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.
+
+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.
+
 Handlers are looked up in the user namespace the struct_ops map was
 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.
 
-The ``F`` flag cannot be combined with ``B`` entries: it pre-opens a fixed
-interpreter at registration time and a ``B`` entry has none. The ``C`` flag
-works as it does for a static entry: the interpreter runs with the matched
-binary's credentials, bounded to user namespaces that map the binary's owner
-just like any other setuid exec.
-
 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
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 3fbc51197c76..a3175521c8dd 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -283,6 +283,7 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
 	/* A failing load leaves nothing behind for later entries. */
 	kfree(bprm->bpf_interp_arg);
 	bprm->bpf_interp_arg = NULL;
+	bprm->bpf_flags = 0;
 	return ERR_PTR(retval);
 }
 
@@ -295,6 +296,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	const char *interpreter;
 	struct file *interp_file;
 	struct binfmt_misc *misc;
+	bool preserve_argv0;
 	int retval;
 
 	misc = current_binfmt_misc();
@@ -313,7 +315,32 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (IS_ERR(interpreter))
 		return PTR_ERR(interpreter);
 
-	if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) {
+	/*
+	 * 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;
+	}
+
+	/* The entry's own choice - not one accumulated from an earlier level. */
+	if (preserve_argv0) {
 		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
 	} else {
 		retval = remove_arg_zero(bprm);
@@ -321,9 +348,6 @@ static int load_misc_binary(struct linux_binprm *bprm)
 			return retval;
 	}
 
-	if (fmt->flags & MISC_FMT_OPEN_BINARY)
-		bprm->have_execfd = 1;
-
 	/* make the binary the last argument to the interpreter */
 	retval = copy_string_kernel(bprm->interp, bprm);
 	if (retval < 0)
@@ -372,8 +396,6 @@ static int load_misc_binary(struct linux_binprm *bprm)
 		return PTR_ERR(interp_file);
 
 	bprm->interpreter = interp_file;
-	if (fmt->flags & MISC_FMT_CREDENTIALS)
-		bprm->execfd_creds = 1;
 	return 0;
 }
 
@@ -650,14 +672,14 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
 		return ERR_PTR(-EINVAL);
 
 	/*
-	 * 'F' pre-opens a fixed interpreter at registration time which is
-	 * meaningless for a per-exec computed path. 'C' is fine: it honors the
-	 * suid bits of the matched binary exactly like a static entry, gated by
-	 * the same vfsuid_has_mapping() check in bprm_fill_uid() that keeps the
-	 * transition to uids mapped in the caller's user namespace.
+	 * 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.
 	 */
 	if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
-	    (e->flags & MISC_FMT_OPEN_FILE))
+	    (e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY |
+			 MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
 		return ERR_PTR(-EINVAL);
 
 	return no_free_ptr(e);

-- 
2.53.0


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

* [PATCH v2 8/9] binfmt_misc: let a bpf handler run the interpreter transparently
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (6 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 7/9] binfmt_misc: let a bpf handler choose the invocation flags per exec Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-14 19:58 ` [PATCH v2 9/9] selftests/exec: add binfmt_misc bpf-backed handler test Christian Brauner
  2026-07-15  0:28 ` [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Farid Zakaria
  9 siblings, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, 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, the binary's path is appended as an
argument and /proc/pid/cmdline shows both. For wine or qemu-user that is
the point, but 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, so
load_misc_binary() refuses upfront with -ENOENT.

Add BPF_BINPRM_TRANSPARENT. It goes beyond the static flags: the binary
is handed to the interpreter through AT_EXECFD as with BPF_BINPRM_EXECFD,
but the argument vector and bprm->interp are also left exactly as the
caller set them, so argv[0] and /proc/pid/cmdline look like a direct
execution of the binary. The interpreter has to load the binary from
AT_EXECFD for this; a relocatable loader can, glibc's ld.so does not.

The inaccessible-path bail moves after the load program has run and into
the path-building branch: a transparent interpreter takes the binary from
AT_EXECFD instead of a path, so the restriction no longer applies and the
O_CLOEXEC execveat() case above just works. BPF_BINPRM_PRESERVE_ARGV0 is
subsumed - transparency preserves the whole argument vector, argv[0]
included - and a staged interpreter argument is dropped: no argv slot is
built for it to land in.

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

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 7f42abf9cfba..b446b3be9628 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -148,6 +148,15 @@ 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. It has no
+  static counterpart: 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. This is also the only way a binfmt_misc handler
+  can run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor
+  to ``execveat()``, which otherwise fails because 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 a3175521c8dd..9d7fc598d8c1 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -296,6 +296,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	const char *interpreter;
 	struct file *interp_file;
 	struct binfmt_misc *misc;
+	bool transparent = false;
 	bool preserve_argv0;
 	int retval;
 
@@ -307,10 +308,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);
@@ -326,10 +323,13 @@ static int load_misc_binary(struct linux_binprm *bprm)
 		/* Clear so it can't accumulate into a nested interpreter level. */
 		bprm->bpf_flags = 0;
 
-		preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0;
+		transparent = f & BPF_BINPRM_TRANSPARENT;
+		/* In transparent mode argv[0] is already the caller's. */
+		preserve_argv0 = (f & BPF_BINPRM_PRESERVE_ARGV0) && !transparent;
 		if (f & BPF_BINPRM_CREDENTIALS)
 			bprm->execfd_creds = 1;
-		if (f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD))
+		if (f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD |
+			 BPF_BINPRM_TRANSPARENT))
 			bprm->have_execfd = 1;
 	} else {
 		preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0;
@@ -339,45 +339,63 @@ static int load_misc_binary(struct linux_binprm *bprm)
 			bprm->have_execfd = 1;
 	}
 
-	/* The entry's own choice - not one accumulated from an earlier level. */
-	if (preserve_argv0) {
-		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
+	if (transparent) {
+		/*
+		 * Transparent execution: the interpreter takes the binary from
+		 * AT_EXECFD, so leave the argument vector and bprm->interp as
+		 * the caller set them. argv[0] and /proc/pid/cmdline then look
+		 * like a direct execution of the binary, and a binary passed as
+		 * an inaccessible O_CLOEXEC fd to execveat() can be run at all.
+		 */
+
+		/* No argv is built, so drop any staged interpreter argument. */
+		kfree(bprm->bpf_interp_arg);
+		bprm->bpf_interp_arg = NULL;
 	} else {
-		retval = remove_arg_zero(bprm);
-		if (retval)
-			return retval;
-	}
+		/* The interpreter has to be able to load the binary by path. */
+		if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
+			return -ENOENT;
 
-	/* make the binary the last argument to the interpreter */
-	retval = copy_string_kernel(bprm->interp, bprm);
-	if (retval < 0)
-		return retval;
-	bprm->argc++;
+		/* The entry's own choice - not one accumulated from an earlier level. */
+		if (preserve_argv0) {
+			bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
+		} else {
+			retval = remove_arg_zero(bprm);
+			if (retval)
+				return retval;
+		}
 
-	/*
-	 * A single optional argument to the interpreter, inserted between it
-	 * and the binary just like the argument of a #! interpreter line.
-	 */
-	if (bprm->bpf_interp_arg) {
-		retval = copy_string_kernel(bprm->bpf_interp_arg, bprm);
+		/* make the binary the last argument to the interpreter */
+		retval = copy_string_kernel(bprm->interp, bprm);
 		if (retval < 0)
 			return retval;
 		bprm->argc++;
-		/* Consumed - don't let it leak into a nested interpreter's argv. */
-		kfree(bprm->bpf_interp_arg);
-		bprm->bpf_interp_arg = NULL;
-	}
 
-	/* add the interp as argv[0] */
-	retval = copy_string_kernel(interpreter, bprm);
-	if (retval < 0)
-		return retval;
-	bprm->argc++;
+		/*
+		 * A single optional argument to the interpreter, inserted
+		 * between it and the binary like a #! interpreter line's.
+		 */
+		if (bprm->bpf_interp_arg) {
+			retval = copy_string_kernel(bprm->bpf_interp_arg, bprm);
+			if (retval < 0)
+				return retval;
+			bprm->argc++;
+			/* Consumed - don't leak it into a nested interpreter's argv. */
+			kfree(bprm->bpf_interp_arg);
+			bprm->bpf_interp_arg = NULL;
+		}
 
-	/* Update interp in case binfmt_script needs it. */
-	retval = bprm_change_interp(interpreter, bprm);
-	if (retval < 0)
-		return retval;
+		/* add the interp as argv[0] */
+		retval = copy_string_kernel(interpreter, bprm);
+		if (retval < 0)
+			return retval;
+		bprm->argc++;
+
+		/* Update interp in case binfmt_script needs it. */
+		retval = bprm_change_interp(interpreter, bprm);
+		if (retval < 0)
+			return retval;
+	}
 
 	if (fmt->flags & MISC_FMT_OPEN_FILE) {
 		interp_file = file_clone_open(fmt->interp_file);
diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c
index 00c787e8bdcc..fb5d0f465ccc 100644
--- a/fs/binfmt_misc_bpf.c
+++ b/fs/binfmt_misc_bpf.c
@@ -178,7 +178,9 @@ __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
  */
@@ -186,7 +188,7 @@ __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;
 
 	bprm->bpf_flags = flags;
diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h
index d3112a00cc19..c43df749e021 100644
--- a/include/linux/binfmt_misc.h
+++ b/include/linux/binfmt_misc.h
@@ -16,6 +16,8 @@ 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; implies execfd
  *
  * 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 +26,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] 20+ messages in thread

* [PATCH v2 9/9] selftests/exec: add binfmt_misc bpf-backed handler test
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (7 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 8/9] binfmt_misc: let a bpf handler run the interpreter transparently Christian Brauner
@ 2026-07-14 19:58 ` Christian Brauner
  2026-07-15  0:28 ` [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Farid Zakaria
  9 siblings, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-14 19:58 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail, Christian Brauner (Amutable)

From: Farid Zakaria <farid.m.zakaria@gmail.com>

Exercise the bpf-backed ('B') binfmt_misc handlers end to end. A handler
is a struct binfmt_misc_ops struct_ops map; the test loads and attaches
it (which publishes it by name), activates it with a 'B' entry, and
checks that a matched binary is routed to the interpreter the program
selected via bpf_binprm_set_interp().

Two self-contained cases are covered:

  - bpf_interp: the match program matches a synthetic aarch64 ELF header
    from the prefetched bprm->buf and the load program routes it to a
    fixed interpreter of its choosing.
  - nix_origin: the match program parses the program headers to 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. The
    relocatable binary is linked with PT_INTERP set to the literal
    "$ORIGIN/binfmt_bpf_interp" (-Wl,--dynamic-linker), which the kernel
    cannot resolve on its own.

Both route to a small test interpreter that prints a marker, proving the
program-selected interpreter actually ran.

The bpf objects are compiled against the running kernel's BTF: the
Makefile generates vmlinux.h with bpftool and the harness links libbpf.
Override CLANG/BPFTOOL/VMLINUX_BTF/LIBBPF_CFLAGS/LIBBPF_LDLIBS as needed.
The bpf pieces are only built when clang, bpftool, the vmlinux BTF and
libbpf are all present (HAVE_BPF_TOOLCHAIN=y forces them) so the other
exec selftests keep building without a bpf toolchain.

Christian Brauner (Amutable) <brauner@kernel.org> says:

Adapted to the two-op contract: 'B' entries carry the handler name in
the interpreter field, both programs are sleepable, the match programs
decide. nix_origin reads PT_INTERP from the match program and load
returns zero on success. Skip on kernels without binfmt_misc_ops in BTF.
Build the bpf pieces only when the toolchain is present and gitignore
the generated artifacts.

Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/exec/.gitignore          |   5 +
 tools/testing/selftests/exec/Makefile            |  48 ++++
 tools/testing/selftests/exec/binfmt_bpf_app.c    |  12 +
 tools/testing/selftests/exec/binfmt_bpf_interp.c |  15 ++
 tools/testing/selftests/exec/binfmt_misc_bpf.c   | 277 +++++++++++++++++++++++
 tools/testing/selftests/exec/bpf_interp.bpf.c    |  61 +++++
 tools/testing/selftests/exec/nix_origin.bpf.c    | 224 ++++++++++++++++++
 7 files changed, 642 insertions(+)

diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore
index 7f3d1ae762ec..8b93b405c424 100644
--- a/tools/testing/selftests/exec/.gitignore
+++ b/tools/testing/selftests/exec/.gitignore
@@ -19,3 +19,8 @@ null-argv
 xxxxxxxx*
 pipe
 S_I*.test
+binfmt_misc_bpf
+binfmt_bpf_interp
+binfmt_bpf_app
+*.bpf.o
+vmlinux.h
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 45a3cfc435cf..ec66c1fecfc0 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -21,6 +21,26 @@ TEST_GEN_PROGS += recursion-depth
 TEST_GEN_PROGS += null-argv
 TEST_GEN_PROGS += check-exec
 
+# 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
+# (HAVE_BPF_TOOLCHAIN=y forces it) so the other exec selftests don't grow
+# a bpf toolchain dependency.
+CLANG ?= clang
+BPFTOOL ?= bpftool
+VMLINUX_BTF ?= /sys/kernel/btf/vmlinux
+HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
+			command -v $(BPFTOOL) >/dev/null 2>&1 && \
+			test -r $(VMLINUX_BTF) && \
+			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 += binfmt_bpf_interp binfmt_bpf_app
+else
+$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
+endif
+
 EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx*	\
 	       $(OUTPUT)/S_I*.test
 
@@ -55,3 +75,31 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
 	cp $< $@
 $(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
 	cp $< $@
+
+# --- binfmt_misc bpf ('B') handler test ---------------------------------
+# The struct_ops bpf objects are compiled against the running kernel's BTF.
+# CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check;
+# override LIBBPF_CFLAGS/LDLIBS to point at a libbpf install.
+BPF_CFLAGS ?= -I$(OUTPUT)
+LIBBPF_CFLAGS ?=
+LIBBPF_LDLIBS ?= -lbpf -lelf -lz
+
+$(OUTPUT)/vmlinux.h:
+	$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
+	sed -i '/__ksym;$$/d' $@
+
+$(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
+	$(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@
+
+$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_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
+	$(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
diff --git a/tools/testing/selftests/exec/binfmt_bpf_app.c b/tools/testing/selftests/exec/binfmt_bpf_app.c
new file mode 100644
index 000000000000..472270f148bc
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bpf_app.c
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A relocatable binary for the binfmt_misc_bpf $ORIGIN case. The Makefile
+ * links it with PT_INTERP set to the literal "$ORIGIN/binfmt_bpf_interp"
+ * (-Wl,--dynamic-linker), which the kernel ELF loader cannot resolve. The
+ * nix_origin bpf handler resolves it relative to this binary's directory and
+ * routes execution to the co-located interpreter.
+ */
+int main(void)
+{
+	return 0;
+}
diff --git a/tools/testing/selftests/exec/binfmt_bpf_interp.c b/tools/testing/selftests/exec/binfmt_bpf_interp.c
new file mode 100644
index 000000000000..2db205f095b2
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bpf_interp.c
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test interpreter for the binfmt_misc_bpf selftest. A bpf-backed 'B' handler
+ * routes a matched binary here; printing this marker proves the program's
+ * chosen interpreter actually ran.
+ */
+#include <unistd.h>
+
+int main(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+	write(1, "BPF_INTERP_RAN\n", 15);
+	return 0;
+}
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
new file mode 100644
index 000000000000..cb89d2766fe2
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -0,0 +1,277 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Selftest for binfmt_misc bpf-backed ('B') handlers.
+ *
+ * A handler is a struct binfmt_misc_ops struct_ops map with a sleepable match
+ * and a sleepable load program. Attaching it publishes it by name in the
+ * caller's user namespace; a 'B' entry referencing it by name in the
+ * interpreter field activates it:
+ *
+ *     echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
+ *
+ * Two 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
+ *      fixed interpreter of its choosing.
+ *   2. nix_origin: the match program reads the binary's program headers to
+ *      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).
+ *
+ * Both route to a test interpreter that prints BPF_INTERP_RAN, proving the
+ * program's chosen interpreter actually ran.
+ */
+#define _GNU_SOURCE
+#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>
+
+#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)
+{
+	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 */
+
+	fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
+	if (fd < 0)
+		return -1;
+	if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) {
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
+static int register_entry(const char *name, const char *handler)
+{
+	char rule[128];
+	int fd;
+	ssize_t n;
+
+	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);
+	}
+}
+
+static int check_output(const char *cmd, const char *expected)
+{
+	char buf[128];
+	FILE *fp;
+
+	fp = popen(cmd, "r");
+	if (!fp)
+		return -1;
+	if (!fgets(buf, sizeof(buf), fp)) {
+		pclose(fp);
+		return -1;
+	}
+	pclose(fp);
+	return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
+}
+
+/*
+ * 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.
+ */
+static int run_case(const char *objfile, const char *handler,
+		    const char *entry, const char *target, const char *expect)
+{
+	struct bpf_object *obj;
+	struct bpf_map *map;
+	struct bpf_link *link;
+	int ret = -1;
+
+	obj = bpf_object__open_file(objfile, NULL);
+	if (!obj || libbpf_get_error(obj)) {
+		fprintf(stderr, "open %s failed\n", objfile);
+		return -1;
+	}
+	if (bpf_object__load(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;
+	}
+	link = bpf_map__attach_struct_ops(map);
+	if (!link || libbpf_get_error(link)) {
+		fprintf(stderr, "attach struct_ops '%s' failed\n", handler);
+		goto close;
+	}
+	if (register_entry(entry, handler)) {
+		fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
+		goto detach;
+	}
+	ret = check_output(target, expect);
+	unregister_entry(entry);
+detach:
+	bpf_link__destroy(link);
+close:
+	bpf_object__close(obj);
+	return ret;
+}
+
+int main(void)
+{
+	char src[600], obj[600], appdst[600], interpdst[600];
+	char exe[512];
+	ssize_t n;
+	int fail = 0;
+	struct stat st;
+	struct btf *btf;
+
+	if (getuid() != 0) {
+		fprintf(stderr, "Skipping: test must be run as root\n");
+		return 4; /* KSFT_SKIP */
+	}
+
+	/* 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 */
+	}
+	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);
+
+	/* 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;
+	}
+
+	/* 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;
+	}
+	unlink(AARCH64_PATH);
+
+	/* 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);
+
+	if (!fail)
+		printf("[*] all binfmt_misc bpf cases passed\n");
+	return fail;
+}
diff --git a/tools/testing/selftests/exec/bpf_interp.bpf.c b/tools/testing/selftests/exec/bpf_interp.bpf.c
new file mode 100644
index 000000000000..8df2d2d01e25
--- /dev/null
+++ b/tools/testing/selftests/exec/bpf_interp.bpf.c
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binfmt_misc_ops handler for the selftest's fixed-interpreter case: match a
+ * 64-bit aarch64 ELF header from the prefetched buffer and route it to a fixed
+ * interpreter chosen by the program. This is the portable, self-contained
+ * equivalent of routing a foreign binary to an emulator: it matches
+ * programmatically and computes the interpreter, but points at a test binary
+ * the harness installs rather than a system emulator.
+ */
+#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_AARCH64	183
+
+extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
+				 size_t path__sz) __ksym;
+
+/*
+ * A magic-style decision needs nothing beyond the prefetched bprm->buf,
+ * even though the match program could read the file.
+ */
+SEC("struct_ops.s/match")
+bool BPF_PROG(bpf_interp_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_AARCH64;
+}
+
+SEC("struct_ops.s/load")
+int BPF_PROG(bpf_interp_load, struct linux_binprm *bprm)
+{
+	/*
+	 * Keep the path on the (writable) stack: bpf_binprm_set_interp() takes
+	 * a sized memory arg and the verifier rejects a read-only .rodata
+	 * buffer for it. The harness installs the interpreter at this path.
+	 */
+	char interp[] = "/tmp/binfmt_bpf_interp";
+
+	/* @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 bpf_interp = {
+	.match = (void *)bpf_interp_match,
+	.load = (void *)bpf_interp_load,
+	.name = "bpf_interp",
+};
diff --git a/tools/testing/selftests/exec/nix_origin.bpf.c b/tools/testing/selftests/exec/nix_origin.bpf.c
new file mode 100644
index 000000000000..378e22a4c43b
--- /dev/null
+++ b/tools/testing/selftests/exec/nix_origin.bpf.c
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * nix_origin.bpf.c - $ORIGIN-relative PT_INTERP resolution
+ *
+ * A binfmt_misc_ops handler that makes relocatable (Nix-style) ELF
+ * binaries work: if PT_INTERP starts with "$ORIGIN/", the loader is
+ * resolved relative to the directory of the binary being executed and
+ * selected via bpf_binprm_set_interp(). The match program reads the
+ * program headers itself, so anything else never commits to this
+ * handler and passes through untouched.
+ *
+ * Activate with:
+ *   bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
+ *   echo ':nix-origin:B::::nix_origin:' > /proc/sys/fs/binfmt_misc/register
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+#define PATH_MAX	4096
+#define EI_CLASS	4
+#define ELFCLASSXX	2	/* ELFCLASS64; flip to 1 for 32-bit */
+#define PT_INTERP	3
+#define MAX_PHDRS	64
+
+#define ORIGIN		"$ORIGIN"
+#define ORIGIN_LEN	(sizeof(ORIGIN) - 1)
+
+#define ENOENT		2
+#define ENOEXEC		8
+#define ENAMETOOLONG	36
+
+extern int bpf_dynptr_from_file(struct file *file, __u32 flags,
+				struct bpf_dynptr *ptr__uninit) __ksym;
+extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym;
+extern int bpf_path_d_path(const struct path *path, char *buf,
+			   size_t buf__sz) __ksym;
+extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
+				 size_t path__sz) __ksym;
+
+struct scratch {
+	char interp[PATH_MAX];	/* PT_INTERP as embedded in the binary */
+	char path[PATH_MAX];	/* d_path of the binary, becomes the result */
+};
+
+/* Keyed by pid: execs run concurrently and the programs can sleep. */
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(max_entries, 512);
+	__type(key, __u64);
+	__type(value, struct scratch);
+} scratch_map SEC(".maps");
+
+static const struct scratch zero_scratch;
+
+/* An ELF64 binary per the prefetched header? */
+static bool is_elf64(struct linux_binprm *bprm)
+{
+	return bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' &&
+	       bprm->buf[2] == 'L' && bprm->buf[3] == 'F' &&
+	       bprm->buf[EI_CLASS] == ELFCLASSXX;
+}
+
+/* Locate PT_INTERP; false if the file has none or looks malformed. */
+static bool find_pt_interp(struct bpf_dynptr *dp, struct elf64_phdr *phdr)
+{
+	struct elf64_hdr ehdr;
+	bool found = false;
+	int i;
+
+	if (bpf_dynptr_read(&ehdr, sizeof(ehdr), dp, 0, 0))
+		return false;
+	if (ehdr.e_phentsize != sizeof(struct elf64_phdr))
+		return false;
+
+	bpf_for(i, 0, ehdr.e_phnum) {
+		if (i >= MAX_PHDRS)
+			break;
+		if (bpf_dynptr_read(phdr, sizeof(*phdr), dp,
+				    ehdr.e_phoff + i * sizeof(*phdr), 0))
+			return false;
+		if (phdr->p_type == PT_INTERP) {
+			found = true;
+			break;
+		}
+	}
+	return found;
+}
+
+/*
+ * An ELF64 binary whose PT_INTERP starts with "$ORIGIN/" is ours. The
+ * match can sleep and read the file, so the decision is made here and
+ * regular binaries never commit to this handler: later binfmt_misc
+ * entries and binfmt_elf see them as if we did not exist.
+ */
+SEC("struct_ops.s/match")
+bool BPF_PROG(nix_origin_match, struct linux_binprm *bprm)
+{
+	char prefix[ORIGIN_LEN + 1] = {};
+	struct elf64_phdr phdr;
+	struct bpf_dynptr dp;
+	bool ours = false;
+
+	if (!is_elf64(bprm))
+		return false;
+
+	/* The dynptr must be discarded on every path once requested. */
+	if (bpf_dynptr_from_file(bprm->file, 0, &dp))
+		goto out;
+	if (find_pt_interp(&dp, &phdr) &&
+	    phdr.p_filesz > ORIGIN_LEN + 1 &&
+	    !bpf_dynptr_read(prefix, sizeof(prefix), &dp, phdr.p_offset, 0))
+		ours = !bpf_strncmp(prefix, sizeof(prefix), ORIGIN "/");
+out:
+	bpf_dynptr_file_discard(&dp);
+	return ours;
+}
+
+/*
+ * The match is committed and already vetted the "$ORIGIN/" prefix, so
+ * everything here reads the file again from scratch: -ENOEXEC only
+ * covers a binary that changed under us and stopped being ours.
+ */
+SEC("struct_ops.s/load")
+int BPF_PROG(nix_origin_load, struct linux_binprm *bprm)
+{
+	__u32 isz, sfx, rsz, slash;
+	struct elf64_phdr phdr;
+	struct bpf_dynptr dp;
+	struct scratch *sc;
+	__u64 id;
+	int ret = -ENOEXEC, len, i;
+
+	if (bpf_dynptr_from_file(bprm->file, 0, &dp))
+		goto out;
+
+	if (!find_pt_interp(&dp, &phdr))
+		goto out;
+
+	isz = phdr.p_filesz;
+	if (isz <= ORIGIN_LEN + 1 || isz >= sizeof(sc->interp))
+		goto out;
+	/*
+	 * The range check above compiles to a test on a zero-extended copy of
+	 * the u64 p_filesz, so the verifier does not carry the bound to the
+	 * dynptr_read() length below ("unbounded memory access"). Mask isz to
+	 * the buffer size (a power of two) and force the masked value to be
+	 * materialized with a barrier so the read uses the bounded register.
+	 */
+	isz &= sizeof(sc->interp) - 1;
+	barrier_var(isz);
+
+	id = bpf_get_current_pid_tgid();
+	if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY))
+		goto out;
+	sc = bpf_map_lookup_elem(&scratch_map, &id);
+	if (!sc)
+		goto out_del;
+
+	if (bpf_dynptr_read(sc->interp, isz, &dp, phdr.p_offset, 0))
+		goto out_del;
+	if (sc->interp[isz - 1] != '\0')
+		goto out_del;
+
+	/* Not "$ORIGIN/..." anymore? Then it is not ours anymore either. */
+	if (sc->interp[0] != '$' || sc->interp[1] != 'O' ||
+	    sc->interp[2] != 'R' || sc->interp[3] != 'I' ||
+	    sc->interp[4] != 'G' || sc->interp[5] != 'I' ||
+	    sc->interp[6] != 'N' || sc->interp[7] != '/')
+		goto out_del;
+
+	/*
+	 * From here on resolution failures fail the exec instead of falling
+	 * back to binfmt_elf, which would resolve the literal "$ORIGIN/..."
+	 * relative to the caller's cwd.
+	 */
+	ret = -ENOENT;
+	len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path));
+	if (len <= 0 || len > sizeof(sc->path))
+		goto out_del;
+	/* Unreachable or unlinked ("... (deleted)") binaries can't resolve. */
+	if (sc->path[0] != '/')
+		goto out_del;
+
+	/* $ORIGIN = dirname of the binary. */
+	slash = 0;
+	bpf_for(i, 1, len - 1) {
+		if (i >= sizeof(sc->path))
+			break;
+		if (sc->path[i] == '/')
+			slash = i;
+	}
+
+	/* Splice the suffix (leading '/' and NUL included) onto the dir. */
+	sfx = isz - ORIGIN_LEN;
+	rsz = slash + sfx;
+	if (rsz > sizeof(sc->path)) {
+		ret = -ENAMETOOLONG;
+		goto out_del;
+	}
+	bpf_for(i, 0, sfx) {
+		__u32 s = ORIGIN_LEN + i, d = slash + i;
+
+		if (s >= sizeof(sc->interp) || d >= sizeof(sc->path))
+			break;
+		sc->path[d] = sc->interp[s];
+	}
+
+	ret = bpf_binprm_set_interp(bprm, sc->path, rsz);
+out_del:
+	bpf_map_delete_elem(&scratch_map, &id);
+out:
+	bpf_dynptr_file_discard(&dp);
+	return ret;
+}
+
+SEC(".struct_ops.link")
+struct binfmt_misc_ops nix_origin = {
+	.match = (void *)nix_origin_match,
+	.load = (void *)nix_origin_load,
+	.name = "nix_origin",
+};

-- 
2.53.0


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

* Re: [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
  2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
                   ` (8 preceding siblings ...)
  2026-07-14 19:58 ` [PATCH v2 9/9] selftests/exec: add binfmt_misc bpf-backed handler test Christian Brauner
@ 2026-07-15  0:28 ` Farid Zakaria
  2026-07-15  8:40   ` Christian Brauner
  9 siblings, 1 reply; 20+ messages in thread
From: Farid Zakaria @ 2026-07-15  0:28 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Farid Zakaria, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

On Tue, 14 Jul 2026 21:58:05 +0200, Christian Brauner <brauner@kernel.org> wrote:
> [...]
> 		.load = (void *)nix_load,
> 		.name = "nix",
> 	};
> 
> Farid, this should slot underneath your qemu demo from [4] with the
> program ported to struct_ops. Feel free to take it from here.

Thanks for the recent update.
Is it still "take it from here" or was this left-over from the previous
series.

I think you've done the majority of the work here with the validation of
the few selftests and mentioning the gap for shebangs.

Please let me know what you would like me to do next. I just tested
reviewing the series with b4 + AI -- pretty cool. Was a little clunky to
get it to work but overall a good experience. (I need to tinker with it
a little for NixOS vim setup etc..)

I shared this patch series at my workplace as well. 
I think there is some interest in the work.
(If you are curious as to why, I can elaborate on that as well).

I hope you are enjoying your vacation.

-- 
Farid Zakaria <farid.m.zakaria@gmail.com>

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

* Re: [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm
  2026-07-14 19:58 ` [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm Christian Brauner
@ 2026-07-15  0:28   ` Farid Zakaria
  0 siblings, 0 replies; 20+ messages in thread
From: Farid Zakaria @ 2026-07-15  0:28 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Farid Zakaria, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

On Tue, 14 Jul 2026 21:58:06 +0200, Christian Brauner <brauner@kernel.org> wrote:
> The upcoming bpf-backed binfmt_misc handlers decide how a binary is run
> programmatically at exec time: the interpreter itself, an optional
> single argument to pass to it, and the invocation flags that a static
> binfmt_misc entry fixes at registration time. The selection runs before
> load_misc_binary() has copied the binary path from bprm->interp into
> the argument vector, so the selecting program cannot go through
> bprm_change_interp() directly without clobbering argv[1].
> 
> [...]

Reviewed-by: Farid Zakaria <farid.m.zakaria@gmail.com>

-- 
Farid Zakaria <farid.m.zakaria@gmail.com>

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

* Re: [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries
  2026-07-14 19:58 ` [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries Christian Brauner
@ 2026-07-15  0:28   ` Farid Zakaria
  0 siblings, 0 replies; 20+ messages in thread
From: Farid Zakaria @ 2026-07-15  0:28 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Farid Zakaria, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

On Tue, 14 Jul 2026 21:58:09 +0200, Christian Brauner <brauner@kernel.org> wrote:
> diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
> index f7264b136..a8f641c4e 100644
> --- a/fs/binfmt_misc.c
> +++ b/fs/binfmt_misc.c
> @@ -226,12 +240,51 @@ static struct binfmt_misc *current_binfmt_misc(void)
> [ ... skip 34 lines ... ]
> +	/* Selecting an interpreter is part of the contract. */
> +	if (!bprm->bpf_interp)
> +		return ERR_PTR(-ENOEXEC);
> +
> +	return bprm->bpf_interp;
> +}

The "contract" says that if the BPF program returns 0, it must take
ownership of selecting the interpreter. However the bpf_interp can be
NULL here and therefore is indistinguishable from the refuse to handle
case.

We might want to treat 0-without-interp as a hard -ENOEXEC-to-caller.

Reviewed-by: Farid Zakaria <farid.m.zakaria@gmail.com>

-- 
Farid Zakaria <farid.m.zakaria@gmail.com>

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

* Re: [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs
  2026-07-14 19:58 ` [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs Christian Brauner
@ 2026-07-15  0:28   ` Farid Zakaria
  0 siblings, 0 replies; 20+ messages in thread
From: Farid Zakaria @ 2026-07-15  0:28 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Farid Zakaria, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

On Tue, 14 Jul 2026 21:58:10 +0200, Christian Brauner <brauner@kernel.org> wrote:
> The fs kfuncs are currently exclusive to LSM programs. A binfmt_misc
> handler needs a subset of them to do anything interesting: computing an
> interpreter relative to the binary's location wants bpf_path_d_path()
> on bprm->file->f_path from the load program, and matching on per-binary
> metadata wants bpf_get_file_xattr() and friends right from the match
> program.
> 
> [...]

Reviewed-by: Farid Zakaria <farid.m.zakaria@gmail.com>

-- 
Farid Zakaria <farid.m.zakaria@gmail.com>

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

* Re: [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter
  2026-07-14 19:58 ` [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter Christian Brauner
@ 2026-07-15  0:28   ` Farid Zakaria
  0 siblings, 0 replies; 20+ messages in thread
From: Farid Zakaria @ 2026-07-15  0:28 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Farid Zakaria, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

On Tue, 14 Jul 2026 21:58:11 +0200, Christian Brauner <brauner@kernel.org> wrote:
> A bpf binfmt_misc handler selects an interpreter but, unlike binfmt_script,
> load_misc_binary() builds the argument vector as just [interpreter, binary,
> ...] with no slot for an argument to the interpreter. A handler that wants
> to reproduce a #! line therefore cannot express its single optional
> argument, e.g. a handler that resolves $ORIGIN in a script's #! path loses
> the argument that followed the interpreter.
> 
> [...]

Reviewed-by: Farid Zakaria <farid.m.zakaria@gmail.com>

-- 
Farid Zakaria <farid.m.zakaria@gmail.com>

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

* Re: [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
  2026-07-15  0:28 ` [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Farid Zakaria
@ 2026-07-15  8:40   ` Christian Brauner
  2026-07-16  0:10     ` Farid Zakaria
  0 siblings, 1 reply; 20+ messages in thread
From: Christian Brauner @ 2026-07-15  8:40 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Christian Brauner, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

On 2026-07-14 17:28 -0700, Farid Zakaria wrote:
> On Tue, 14 Jul 2026 21:58:05 +0200, Christian Brauner <brauner@kernel.org> wrote:
> > [...]
> > 		.load = (void *)nix_load,
> > 		.name = "nix",
> > 	};
> > 
> > Farid, this should slot underneath your qemu demo from [4] with the
> > program ported to struct_ops. Feel free to take it from here.
> 
> Thanks for the recent update.
> Is it still "take it from here" or was this left-over from the previous
> series.

Leftover from the previous series. :) Sorry.

> I think you've done the majority of the work here with the validation of
> the few selftests and mentioning the gap for shebangs.

It was a nice joint effort imo. I think this is in general an
interesting idea. I'm also working on some glibc loader patches. One
of the patches in this series allows for "transparent binary execution"
if the loader supports it.

In this mode you can pass AT_EXECFD to an interpreter and don't change
the command line. An exec like this is indistinguishable from a "native"
exec.

> Please let me know what you would like me to do next. I just tested
> reviewing the series with b4 + AI -- pretty cool. Was a little clunky to
> get it to work but overall a good experience. (I need to tinker with it
> a little for NixOS vim setup etc..)
> 
> I shared this patch series at my workplace as well. 
> I think there is some interest in the work.
> (If you are curious as to why, I can elaborate on that as well).

Sure, happy to hear other use-cases. I have a plan to make it possible
to pre-open a set of interpreters (pin them essentially) when
registering the handler in binfmt_misc and then bpf would be able to
select from a set of pre-opened interpreters but that's for the future.

> I hope you are enjoying your vacation.

Thanks!


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

* Re: [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
  2026-07-15  8:40   ` Christian Brauner
@ 2026-07-16  0:10     ` Farid Zakaria
  2026-07-16  0:12       ` Farid Zakaria
  2026-07-16  9:48       ` Christian Brauner
  0 siblings, 2 replies; 20+ messages in thread
From: Farid Zakaria @ 2026-07-16  0:10 UTC (permalink / raw)
  To: Christian Brauner, Farid Zakaria
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail

On Wed Jul 15, 2026 at 1:40 AM PDT, Christian Brauner wrote:
> On 2026-07-14 17:28 -0700, Farid Zakaria wrote:
>> On Tue, 14 Jul 2026 21:58:05 +0200, Christian Brauner <brauner@kernel.org> wrote:
>> > [...]
>> > 		.load = (void *)nix_load,
>> > 		.name = "nix",
>> > 	};
>> > 
>> > Farid, this should slot underneath your qemu demo from [4] with the
>> > program ported to struct_ops. Feel free to take it from here.
>> 
>> Thanks for the recent update.
>> Is it still "take it from here" or was this left-over from the previous
>> series.
>
> Leftover from the previous series. :) Sorry.
>

No problem -- just checking.

>> I think you've done the majority of the work here with the validation of
>> the few selftests and mentioning the gap for shebangs.
>
> It was a nice joint effort imo. I think this is in general an
> interesting idea. I'm also working on some glibc loader patches. One
> of the patches in this series allows for "transparent binary execution"
> if the loader supports it.
>
> In this mode you can pass AT_EXECFD to an interpreter and don't change
> the command line. An exec like this is indistinguishable from a "native"
> exec.
>

This sounds super interesting (orthogonal to binfmt_misc and BPF work right?)
but a neat feature nonetheless. I wonder if this makes it easier for
tools like perf to attribute the correct binary or they have already
handled interpreters in the cmdline well enough.

>> Please let me know what you would like me to do next. I just tested
>> reviewing the series with b4 + AI -- pretty cool. Was a little clunky to
>> get it to work but overall a good experience. (I need to tinker with it
>> a little for NixOS vim setup etc..)
>> 
>> I shared this patch series at my workplace as well. 
>> I think there is some interest in the work.
>> (If you are curious as to why, I can elaborate on that as well).
>
> Sure, happy to hear other use-cases. I have a plan to make it possible
> to pre-open a set of interpreters (pin them essentially) when
> registering the handler in binfmt_misc and then bpf would be able to
> select from a set of pre-opened interpreters but that's for the future.
>

This is assuming that in prior calls to the BPF it returned the path
string to be pinned?

As for the use case, we use buck2 --
a build system similar to Google's Bazel and does many things in spirit
to Nix by leveraging RUN_PATH to create links to the shared-libraries.

We have customized our interpreter however the lack of $ORIGIN means
that we've had to maintain a single glibc versioning across our machines as
a platform since glibc has tight coupling between the loader and the
libc.

This has been a pain point for upgrading. I think the ability for the
loader to now be relative to $ORIGIN meanst hat each binary could
diverge glibc/ld.so and not affect the rest of the machines.

tl;dr; ld.so from $ORIGIN itself is not only useful but since it's
intimately tied to glibc, it allows for more flexibililty across our
builds (very similar to NixOS in spirit).

>> I hope you are enjoying your vacation.
>
> Thanks!


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

* Re: [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
  2026-07-16  0:10     ` Farid Zakaria
@ 2026-07-16  0:12       ` Farid Zakaria
  2026-07-16  9:42         ` Christian Brauner
  2026-07-16  9:48       ` Christian Brauner
  1 sibling, 1 reply; 20+ messages in thread
From: Farid Zakaria @ 2026-07-16  0:12 UTC (permalink / raw)
  To: Farid Zakaria, Christian Brauner
  Cc: Daniel Borkmann, Alexei Starovoitov, Kees Cook, Alexander Viro,
	Jan Kara, Jonathan Corbet, linux-fsdevel, linux-mm, bpf, jannh,
	mail

On Wed Jul 15, 2026 at 5:10 PM PDT, Farid Zakaria wrote:
> On Wed Jul 15, 2026 at 1:40 AM PDT, Christian Brauner wrote:
>> On 2026-07-14 17:28 -0700, Farid Zakaria wrote:
>>> On Tue, 14 Jul 2026 21:58:05 +0200, Christian Brauner <brauner@kernel.org> wrote:
>>> > [...]
>>> > 		.load = (void *)nix_load,
>>> > 		.name = "nix",
>>> > 	};
>>> > 
>>> > Farid, this should slot underneath your qemu demo from [4] with the
>>> > program ported to struct_ops. Feel free to take it from here.
>>> 
>>> Thanks for the recent update.
>>> Is it still "take it from here" or was this left-over from the previous
>>> series.
>>
>> Leftover from the previous series. :) Sorry.
>>
>
> No problem -- just checking.
>
>>> I think you've done the majority of the work here with the validation of
>>> the few selftests and mentioning the gap for shebangs.
>>
>> It was a nice joint effort imo. I think this is in general an
>> interesting idea. I'm also working on some glibc loader patches. One
>> of the patches in this series allows for "transparent binary execution"
>> if the loader supports it.
>>
>> In this mode you can pass AT_EXECFD to an interpreter and don't change
>> the command line. An exec like this is indistinguishable from a "native"
>> exec.
>>
>
> This sounds super interesting (orthogonal to binfmt_misc and BPF work right?)
> but a neat feature nonetheless. I wonder if this makes it easier for
> tools like perf to attribute the correct binary or they have already
> handled interpreters in the cmdline well enough.
>
>>> Please let me know what you would like me to do next. I just tested
>>> reviewing the series with b4 + AI -- pretty cool. Was a little clunky to
>>> get it to work but overall a good experience. (I need to tinker with it
>>> a little for NixOS vim setup etc..)
>>> 
>>> I shared this patch series at my workplace as well. 
>>> I think there is some interest in the work.
>>> (If you are curious as to why, I can elaborate on that as well).
>>
>> Sure, happy to hear other use-cases. I have a plan to make it possible
>> to pre-open a set of interpreters (pin them essentially) when
>> registering the handler in binfmt_misc and then bpf would be able to
>> select from a set of pre-opened interpreters but that's for the future.
>>
>
> This is assuming that in prior calls to the BPF it returned the path
> string to be pinned?
>
> As for the use case, we use buck2 --
> a build system similar to Google's Bazel and does many things in spirit
> to Nix by leveraging RUN_PATH to create links to the shared-libraries.
>
> We have customized our interpreter however the lack of $ORIGIN means
> that we've had to maintain a single glibc versioning across our machines as
> a platform since glibc has tight coupling between the loader and the
> libc.
>
> This has been a pain point for upgrading. I think the ability for the
> loader to now be relative to $ORIGIN meanst hat each binary could
> diverge glibc/ld.so and not affect the rest of the machines.
>
> tl;dr; ld.so from $ORIGIN itself is not only useful but since it's
> intimately tied to glibc, it allows for more flexibililty across our
> builds (very similar to NixOS in spirit).
>
>>> I hope you are enjoying your vacation.
>>
>> Thanks!

Ah one more thing!

What's next for this patch? Do we wait for BPF maintainers to comment?
Should I continue to add Reviewed-By trailers to the other patches?
(I have limited knowledge on some of the mutex stuff but the agent
review in b4 was helpful in understanding it enough to give a review.)

Sorry if it's obvious. I've never contributed to a patch this far
along or substantial to the point of having to understand the
merging/acceptance process in Linux.

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

* Re: [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
  2026-07-16  0:12       ` Farid Zakaria
@ 2026-07-16  9:42         ` Christian Brauner
  0 siblings, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-16  9:42 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Christian Brauner, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

> Should I continue to add Reviewed-By trailers to the other patches?

Review them, please where you can.

> (I have limited knowledge on some of the mutex stuff but the agent
> review in b4 was helpful in understanding it enough to give a review.)
> 
> Sorry if it's obvious. I've never contributed to a patch this far
> along or substantial to the point of having to understand the
> merging/acceptance process in Linux.

I plan on getting this into -next in the near future.



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

* Re: [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers
  2026-07-16  0:10     ` Farid Zakaria
  2026-07-16  0:12       ` Farid Zakaria
@ 2026-07-16  9:48       ` Christian Brauner
  1 sibling, 0 replies; 20+ messages in thread
From: Christian Brauner @ 2026-07-16  9:48 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Christian Brauner, Daniel Borkmann, Alexei Starovoitov, Kees Cook,
	Alexander Viro, Jan Kara, Jonathan Corbet, linux-fsdevel,
	linux-mm, bpf, jannh, mail

> > In this mode you can pass AT_EXECFD to an interpreter and don't change
> > the command line. An exec like this is indistinguishable from a "native"
> > exec.
> >
> 
> This sounds super interesting (orthogonal to binfmt_misc and BPF work right?)
> but a neat feature nonetheless. I wonder if this makes it easier for
> tools like perf to attribute the correct binary or they have already
> handled interpreters in the cmdline well enough.

If you do gdb -p on a process that went through binfmt_misc it will load
libc symbols by default. With transparent mode it will look exactly like
a native exeuction and gdb will just work. IOW, if you have a loader
that knows to consume a binary just from AT_EXECFD and honors
AT_FLAG_PRESERVE_ARGV (the extension I have patches for) everything
looks exactly like a native execution (ceteris paribus).

> >> Please let me know what you would like me to do next. I just tested
> >> reviewing the series with b4 + AI -- pretty cool. Was a little clunky to
> >> get it to work but overall a good experience. (I need to tinker with it
> >> a little for NixOS vim setup etc..)
> >> 
> >> I shared this patch series at my workplace as well. 
> >> I think there is some interest in the work.
> >> (If you are curious as to why, I can elaborate on that as well).
> >
> > Sure, happy to hear other use-cases. I have a plan to make it possible
> > to pre-open a set of interpreters (pin them essentially) when
> > registering the handler in binfmt_misc and then bpf would be able to
> > select from a set of pre-opened interpreters but that's for the future.
> >
> 
> This is assuming that in prior calls to the BPF it returned the path
> string to be pinned?

It's easier. We just need to extend "register" to add interpreters to an
entry.


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

end of thread, other threads:[~2026-07-16  9:48 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
2026-07-14 19:58 ` [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 2/9] binfmt_misc: add binfmt_misc_ops bpf struct_ops Christian Brauner
2026-07-14 19:58 ` [PATCH v2 3/9] binfmt_misc: let the entry lookup walk sleep Christian Brauner
2026-07-14 19:58 ` [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 7/9] binfmt_misc: let a bpf handler choose the invocation flags per exec Christian Brauner
2026-07-14 19:58 ` [PATCH v2 8/9] binfmt_misc: let a bpf handler run the interpreter transparently Christian Brauner
2026-07-14 19:58 ` [PATCH v2 9/9] selftests/exec: add binfmt_misc bpf-backed handler test Christian Brauner
2026-07-15  0:28 ` [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Farid Zakaria
2026-07-15  8:40   ` Christian Brauner
2026-07-16  0:10     ` Farid Zakaria
2026-07-16  0:12       ` Farid Zakaria
2026-07-16  9:42         ` Christian Brauner
2026-07-16  9:48       ` Christian Brauner

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