Merge patch series "binfmt_misc: bpf-backed binary type handlers"

Christian Brauner <brauner@kernel.org> says:

binfmt_misc: bpf-backed binary type handlers

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

VL;MR (very long, must read):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

A 'B' entry carries no flags in the register string: the load program
chooses the invocation flags per exec through bpf_binprm_set_flags()
instead. BPF_BINPRM_PRESERVE_ARGV0, BPF_BINPRM_CREDENTIALS and
BPF_BINPRM_EXECFD keep the static 'P', 'C' and 'O' semantics -
BPF_BINPRM_CREDENTIALS honors the matched binary's suid bits exactly
as a static 'C' entry does, with the setuid transition gated by
vfsuid_has_mapping() in the caller's user namespace either way, which
makes 'B' handlers usable for a per-binary loader over setuid
binaries. 'F' (pre-open a fixed interpreter) is rejected: a 'B' entry
has no fixed interpreter. AT_EXECVE_CHECK never invokes programs and
interpreter chains stay capped by the usual ELOOP depth.

A handler for the Nix case then looks roughly like:

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

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

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

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

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

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

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

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

* patches from https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-0-57b7529c002c@kernel.org:
  selftests/exec: add binfmt_misc bpf-backed handler test
  binfmt_misc: let a bpf handler choose the invocation flags per exec
  binfmt_misc: let bpf handlers pass an argument to the interpreter
  bpf: allow fs kfuncs for binfmt_misc_ops programs
  binfmt_misc: wire up bpf-backed 'B' entries
  binfmt_misc: let the entry lookup walk sleep
  binfmt_misc: add binfmt_misc_ops bpf struct_ops
  exec: stash bpf-selected interpreter state in struct linux_binprm

Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-0-57b7529c002c@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-07-17 00:36:22 +02:00
16 changed files with 1392 additions and 40 deletions

View File

@@ -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,65 @@ 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.
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.
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.
A handler is looked up only in the user namespace the struct_ops map was
registered in. Handlers are not inherited, so an entry can only reference a
handler registered in the same user namespace as its binfmt_misc instance.
The entry keeps the handler alive; deleting the struct_ops map only prevents
new activations.
To use binfmt_misc you have to mount it first. You can mount it with
``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add
a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your

View File

@@ -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

View File

@@ -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

View File

@@ -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>
@@ -29,6 +30,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>
@@ -38,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. */
@@ -59,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 */
@@ -82,6 +87,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 +119,16 @@ 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. 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 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 +138,26 @@ 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 (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 {
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 +168,27 @@ 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)
{
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);
}
/**
@@ -182,8 +206,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);
}
}
@@ -216,14 +240,64 @@ 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 or flags a previous chain level staged. */
kfree(bprm->bpf_interp);
bprm->bpf_interp = NULL;
bprm->bpf_flags = 0;
retval = e->bpf_ops->load(bprm);
if (retval) {
/* Keep a program-supplied error within errno range. */
if (retval > 0 || retval < -MAX_ERRNO)
retval = -ENOEXEC;
goto drop_staged;
}
/* Selecting an interpreter is part of the contract. */
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;
bprm->bpf_flags = 0;
return ERR_PTR(retval);
}
/*
* 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;
bool preserve_argv0, want_execfd, want_creds;
int retval;
misc = current_binfmt_misc();
@@ -238,7 +312,32 @@ static int load_misc_binary(struct linux_binprm *bprm)
if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
return -ENOENT;
if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) {
interpreter = entry_select_interpreter(fmt, bprm);
if (IS_ERR(interpreter))
return PTR_ERR(interpreter);
/*
* The invocation flags are fixed at registration for a static handler
* and chosen per exec by the load program, via bpf_binprm_set_flags(),
* for a bpf one.
*/
if (test_bit(MISC_FMT_BPF_BIT, &fmt->flags)) {
u64 f = bprm->bpf_flags;
/* Clear so it can't accumulate into a nested interpreter level. */
bprm->bpf_flags = 0;
preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0;
want_creds = f & BPF_BINPRM_CREDENTIALS;
want_execfd = f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD);
} else {
preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0;
want_creds = fmt->flags & MISC_FMT_CREDENTIALS;
want_execfd = fmt->flags & MISC_FMT_OPEN_BINARY;
}
/* 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);
@@ -246,20 +345,34 @@ static int load_misc_binary(struct linux_binprm *bprm)
return retval;
}
/* 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(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;
@@ -274,15 +387,15 @@ 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);
bprm->interpreter = interp_file;
if (fmt->flags & MISC_FMT_OPEN_BINARY)
if (want_execfd)
bprm->have_execfd = 1;
if (fmt->flags & MISC_FMT_CREDENTIALS)
if (want_creds)
bprm->execfd_creds = 1;
return 0;
}
@@ -427,6 +540,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'
@@ -491,13 +625,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);
@@ -510,9 +652,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);
@@ -521,6 +672,17 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
if (p != buf + count)
return ERR_PTR(-EINVAL);
/*
* A bpf handler decides the invocation flags per exec with
* bpf_binprm_set_flags() rather than fixing them at registration, so a
* 'B' entry carries no flags: 'P', 'C' and 'O' become per-exec choices
* and 'F' (pre-open a fixed interpreter) is meaningless for it.
*/
if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
(e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY |
MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
return ERR_PTR(-EINVAL);
return no_free_ptr(e);
}
@@ -574,7 +736,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: ");
@@ -588,7 +753,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);
@@ -669,7 +836,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.
*/
@@ -839,6 +1006,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
@@ -863,6 +1039,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;
}
@@ -1055,6 +1233,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);

351
fs/binfmt_misc_bpf.c Normal file
View File

@@ -0,0 +1,351 @@
// 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
*
* Look for a handler named @name registered in @user_ns. A handler is not
* inherited from ancestor user namespaces: an entry can only name a handler
* registered in the same user namespace as its instance. 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)
{
struct bm_bpf_ops_reg *reg;
guard(spinlock)(&bm_bpf_ops_lock);
reg = bm_bpf_ops_find(user_ns, name);
if (!reg)
return NULL;
if (!bpf_struct_ops_get(reg->ops))
return NULL;
return reg->ops;
}
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;
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
return -EACCES;
/* ->st_ops is unset during the cfg pass; enforced once it is set. */
if (!prog->aux->st_ops)
return 0;
/* Only the load program decides how a binary is run. */
if (bpf_prog_is_binfmt_misc_ops(prog) &&
prog->aux->attach_st_ops_member_off == offsetof(struct binfmt_misc_ops, load))
return 0;
return -EACCES;
}
static const struct btf_kfunc_id_set bm_bpf_kfunc_set = {
.owner = THIS_MODULE,
.set = &bm_bpf_kfunc_ids,
.filter = bm_bpf_kfunc_filter,
};
static bool bm_bpf_ops__match(struct linux_binprm *bprm)
{
return false;
}
static int bm_bpf_ops__load(struct linux_binprm *bprm)
{
return 0;
}
static struct binfmt_misc_ops bm_bpf_ops_stubs = {
.match = bm_bpf_ops__match,
.load = bm_bpf_ops__load,
};
static int bm_bpf_init(struct btf *btf)
{
return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
&bm_bpf_kfunc_set);
}
static int bm_bpf_check_member(const struct btf_type *t,
const struct btf_member *member,
const struct bpf_prog *prog)
{
u32 moff = __btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct binfmt_misc_ops, match):
case offsetof(struct binfmt_misc_ops, load):
/* Reliable file reads at exec time require sleeping. */
if (!prog->sleepable)
return -EINVAL;
break;
}
return 0;
}
static int bm_bpf_init_member(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata)
{
const struct binfmt_misc_ops *uops = udata;
struct binfmt_misc_ops *ops = kdata;
u32 moff = __btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct binfmt_misc_ops, name):
if (bpf_obj_name_cpy(ops->name, uops->name,
sizeof(ops->name)) <= 0)
return -EINVAL;
return 1;
}
return 0;
}
static int bm_bpf_validate(void *kdata)
{
struct binfmt_misc_ops *ops = kdata;
if (!ops->match || !ops->load)
return -EINVAL;
return 0;
}
static int bm_bpf_reg(void *kdata, struct bpf_link *link)
{
struct binfmt_misc_ops *ops = kdata;
struct bm_bpf_ops_reg *reg;
reg = kzalloc_obj(*reg, GFP_KERNEL_ACCOUNT);
if (!reg)
return -ENOMEM;
reg->ops = ops;
reg->link = link;
reg->user_ns = get_user_ns(current_user_ns());
guard(spinlock)(&bm_bpf_ops_lock);
if (bm_bpf_ops_find(reg->user_ns, ops->name)) {
put_user_ns(reg->user_ns);
kfree(reg);
return -EEXIST;
}
list_add(&reg->list, &bm_bpf_ops_list);
return 0;
}
static void bm_bpf_unreg(void *kdata, struct bpf_link *link)
{
struct bm_bpf_ops_reg *reg;
guard(spinlock)(&bm_bpf_ops_lock);
list_for_each_entry(reg, &bm_bpf_ops_list, list) {
if (reg->ops == kdata && reg->link == link) {
list_del(&reg->list);
put_user_ns(reg->user_ns);
kfree(reg);
return;
}
}
}
static const struct bpf_verifier_ops bm_bpf_verifier_ops = {
.get_func_proto = bpf_base_func_proto,
.is_valid_access = bpf_tracing_btf_ctx_access,
};
static struct bpf_struct_ops bpf_binfmt_misc_ops = {
.verifier_ops = &bm_bpf_verifier_ops,
.init = bm_bpf_init,
.check_member = bm_bpf_check_member,
.init_member = bm_bpf_init_member,
.validate = bm_bpf_validate,
.reg = bm_bpf_reg,
.unreg = bm_bpf_unreg,
.cfi_stubs = &bm_bpf_ops_stubs,
.name = "binfmt_misc_ops",
.owner = THIS_MODULE,
};
static int __init bm_bpf_struct_ops_init(void)
{
return register_bpf_struct_ops(&bpf_binfmt_misc_ops, binfmt_misc_ops);
}
late_initcall(bm_bpf_struct_ops_init);

View File

@@ -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>
@@ -392,10 +393,25 @@ BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_real_data_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 (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
return -EACCES;
/* ->st_ops is unset during the cfg pass; enforced once it is set. */
if (!prog->aux->st_ops)
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;
}
@@ -438,7 +454,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);

View File

@@ -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);
}

View File

@@ -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 */

View File

@@ -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;

View File

@@ -19,3 +19,8 @@ null-argv
xxxxxxxx*
pipe
S_I*.test
binfmt_misc_bpf
binfmt_bpf_interp
binfmt_bpf_app
*.bpf.o
vmlinux.h

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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",
};

View File

@@ -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",
};