* [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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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(®->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(®->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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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
[not found] ` <20260715-reorganisation-umtausch-radiergummi-8b38ca81ebb0@brauner>
9 siblings, 1 reply; 17+ 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] 17+ messages in thread