Linux filesystem development
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: Farid Zakaria <farid.m.zakaria@gmail.com>
Cc: Daniel Borkmann <daniel@iogearbox.net>,
	 Alexei Starovoitov <ast@kernel.org>, Kees Cook <kees@kernel.org>,
	 Alexander Viro <viro@zeniv.linux.org.uk>,
	Jan Kara <jack@suse.cz>,  Jonathan Corbet <corbet@lwn.net>,
	linux-fsdevel@vger.kernel.org,  linux-mm@kvack.org,
	linux-kernel@vger.kernel.org, bpf@vger.kernel.org,
	 jannh@google.com, linux-mm@kvack.org, mail@johnericson.me,
	 "Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH RFC POC 3/4] binfmt_misc: wire up bpf-backed 'B' entries
Date: Tue, 07 Jul 2026 21:36:47 +0200	[thread overview]
Message-ID: <20260707-work-bpf-binfmt_misc-v1-3-74b995c84ec1@kernel.org> (raw)
In-Reply-To: <20260707-work-bpf-binfmt_misc-v1-0-74b995c84ec1@kernel.org>

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 field carries the handler name; magic, mask, and interpreter
must be empty since the program supplies both the matching and 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 in put_binfmt_handler() next to the MISC_FMT_OPEN_FILE
interp_file cleanup, which the existing users refcount already defers
past any concurrent load_misc_binary(), and explicitly on the
registration failure path where the users refcount is not live yet.

The program runs from load_misc_binary(), never from the matching walk
under entries_lock: the read_lock disables preemption while the load
program must be able to sleep to read file content. search_binfmt_handler()
therefore only nominates the next enabled 'B' entry and the program
decides outside the lock. A declining program (returning 0) falls
through to handlers registered after it via a skip cursor and a rescan;
entries registered or removed between rescans can shift the cursor, so
a program may be consulted twice in that window, which is harmless
since matching must be free of side effects. Returning a positive value
without having selected an interpreter terminates the binfmt_misc scan
with -ENOEXEC so the remaining binary formats still get a shot.

The 'C' and 'F' flags are rejected for 'B' entries. 'F' exists to
pre-open a fixed interpreter at registration time in the registrar's
context which is meaningless for a per-exec computed path. 'C' honors
the suid bits of the matched binary while executing the interpreter;
combined with a program-chosen interpreter that would let a user
namespace root pick what runs with a setuid binary's credentials. The
computed path itself cannot widen access: it is opened with open_exec()
under the caller's credentials with the usual LSM and noexec checks,
identical to a statically registered interpreter.

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 |  40 ++++++++-
 fs/binfmt_misc.c                          | 143 +++++++++++++++++++++++++++---
 2 files changed, 168 insertions(+), 15 deletions(-)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index c0a34fbf8022..c0efb0773628 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -26,11 +26,13 @@ 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...``).
-   Ignored when using filename extension matching.
+   Ignored when using filename extension matching. For ``B`` entries this
+   field carries the name of the bpf handler instead.
 - ``magic``
    is the byte sequence binfmt_misc is matching for. The magic string
    may contain hex-encoded characters like ``\x0a`` or ``\xA4``. Note that you
@@ -97,6 +99,40 @@ 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 a bpf program. A handler is an instance of the
+``binfmt_misc_ops`` struct_ops with a sleepable ``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 and carries neither magic,
+mask, nor interpreter::
+
+    echo ':qemu:B:my_handler::::' > register
+
+At exec time the ``load`` program receives the ``linux_binprm`` of the
+binary. It can match on the header in ``bprm->buf``, read the file itself,
+e.g. to parse ELF program headers, 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 a
+positive value. Returning ``0`` falls through to the handlers registered
+after this one, a negative errno fails the exec with that error;
+``-ENOEXEC`` ends the binfmt_misc search but 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 registrations.
+
+The ``C`` and ``F`` flags cannot be combined with ``B`` entries: there is no
+fixed interpreter to pre-open and a program-selected interpreter must never
+inherit the credentials of a setuid binary.
+
 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 84349fcb93f1..b80e219ba587 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -17,6 +17,7 @@
 #include <linux/sched/mm.h>
 #include <linux/magic.h>
 #include <linux/binfmts.h>
+#include <linux/binfmt_misc.h>
 #include <linux/slab.h>
 #include <linux/ctype.h>
 #include <linux/string_helpers.h>
@@ -41,7 +42,7 @@ enum {
 	VERBOSE_STATUS = 1 /* make it zero to save 400 bytes kernel memory */
 };
 
-enum {Enabled, Magic};
+enum {Enabled, Magic, Bpf};
 #define MISC_FMT_PRESERVE_ARGV0 (1UL << 31)
 #define MISC_FMT_OPEN_BINARY (1UL << 30)
 #define MISC_FMT_CREDENTIALS (1UL << 29)
@@ -58,6 +59,8 @@ typedef struct {
 	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() */
 } Node;
 
@@ -82,14 +85,19 @@ static struct file_system_type bm_fs_type;
  * search_binfmt_handler - search for a binary handler for @bprm
  * @misc: handle to binfmt_misc instance
  * @bprm: binary for which we are looking for a handler
+ * @bpf_skip: number of bpf-backed handlers to skip over
  *
  * Search for a binary type handler for @bprm in the list of registered binary
- * type handlers.
+ * type handlers. A bpf-backed handler cannot be matched here as its program
+ * must run in sleepable context; it is returned as a candidate and the
+ * program decides in load_misc_binary(). @bpf_skip allows the caller to
+ * resume the search after the first @bpf_skip candidates declined.
  *
  * Return: binary type list entry on success, NULL on failure
  */
 static Node *search_binfmt_handler(struct binfmt_misc *misc,
-				   struct linux_binprm *bprm)
+				   struct linux_binprm *bprm,
+				   unsigned int bpf_skip)
 {
 	char *p = strrchr(bprm->interp, '.');
 	Node *e;
@@ -103,6 +111,15 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc,
 		if (!test_bit(Enabled, &e->flags))
 			continue;
 
+		/* The program decides in load_misc_binary(). */
+		if (test_bit(Bpf, &e->flags)) {
+			if (bpf_skip) {
+				bpf_skip--;
+				continue;
+			}
+			return e;
+		}
+
 		/* Do matching based on extension if applicable. */
 		if (!test_bit(Magic, &e->flags)) {
 			if (p && !strcmp(e->magic, p + 1))
@@ -132,6 +149,7 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc,
  * get_binfmt_handler - try to find a binary type handler
  * @misc: handle to binfmt_misc instance
  * @bprm: binary for which we are looking for a handler
+ * @bpf_skip: number of bpf-backed handlers to skip over
  *
  * 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().
@@ -139,12 +157,13 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc,
  * Return: binary type list entry on success, NULL on failure
  */
 static Node *get_binfmt_handler(struct binfmt_misc *misc,
-				struct linux_binprm *bprm)
+				struct linux_binprm *bprm,
+				unsigned int bpf_skip)
 {
 	Node *e;
 
 	read_lock(&misc->entries_lock);
-	e = search_binfmt_handler(misc, bprm);
+	e = search_binfmt_handler(misc, bprm, bpf_skip);
 	if (e)
 		refcount_inc(&e->users);
 	read_unlock(&misc->entries_lock);
@@ -164,6 +183,8 @@ static void put_binfmt_handler(Node *e)
 	if (refcount_dec_and_test(&e->users)) {
 		if (e->flags & MISC_FMT_OPEN_FILE)
 			filp_close(e->interp_file, NULL);
+		if (e->bpf_ops)
+			binfmt_misc_put_ops(e->bpf_ops);
 		kfree(e);
 	}
 }
@@ -206,12 +227,15 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	struct file *interp_file = NULL;
 	int retval = -ENOEXEC;
 	struct binfmt_misc *misc;
+	const char *interpreter;
+	unsigned int bpf_skip = 0;
 
 	misc = load_binfmt_misc();
 	if (!misc->enabled)
 		return retval;
 
-	fmt = get_binfmt_handler(misc, bprm);
+retry:
+	fmt = get_binfmt_handler(misc, bprm, bpf_skip);
 	if (!fmt)
 		return retval;
 
@@ -220,6 +244,32 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
 		goto ret;
 
+	if (test_bit(Bpf, &fmt->flags)) {
+		retval = fmt->bpf_ops->load(bprm);
+		if (retval < 0) {
+			/* Keep a program-supplied error within errno range. */
+			if (retval < -MAX_ERRNO)
+				retval = -ENOEXEC;
+			goto ret;
+		}
+		if (!retval) {
+			/* Declined, move on to later handlers. */
+			kfree(bprm->bpf_interp);
+			bprm->bpf_interp = NULL;
+			put_binfmt_handler(fmt);
+			bpf_skip++;
+			retval = -ENOEXEC;
+			goto retry;
+		}
+		/* Selecting an interpreter is part of the contract. */
+		retval = -ENOEXEC;
+		if (!bprm->bpf_interp)
+			goto ret;
+		interpreter = bprm->bpf_interp;
+	} else {
+		interpreter = fmt->interpreter;
+	}
+
 	if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) {
 		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
 	} else {
@@ -238,13 +288,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)
 		goto ret;
 	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)
 		goto ret;
 
@@ -253,7 +303,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 		if (!IS_ERR(interp_file))
 			deny_write_access(interp_file);
 	} else {
-		interp_file = open_exec(fmt->interpreter);
+		interp_file = open_exec(interpreter);
 	}
 	retval = PTR_ERR(interp_file);
 	if (IS_ERR(interp_file))
@@ -265,6 +315,9 @@ static int load_misc_binary(struct linux_binprm *bprm)
 
 	retval = 0;
 ret:
+	/* A program-selected interpreter is consumed by this exec attempt. */
+	kfree(bprm->bpf_interp);
+	bprm->bpf_interp = NULL;
 
 	/*
 	 * If we actually put the node here all concurrent calls to
@@ -404,13 +457,47 @@ static Node *create_entry(const char __user *buffer, size_t count)
 		pr_debug("register: type: M (magic)\n");
 		e->flags = (1 << Enabled) | (1 << Magic);
 		break;
+	case 'B':
+		if (!IS_ENABLED(CONFIG_BINFMT_MISC_BPF))
+			goto einval;
+		pr_debug("register: type: B (bpf)\n");
+		e->flags = (1 << Enabled) | (1 << Bpf);
+		break;
 	default:
 		goto einval;
 	}
 	if (*p++ != del)
 		goto einval;
 
-	if (test_bit(Magic, &e->flags)) {
+	if (test_bit(Bpf, &e->flags)) {
+		char *s;
+
+		/* The 'offset' field carries the handler name. */
+		s = strchr(p, del);
+		if (!s)
+			goto einval;
+		*s++ = '\0';
+		e->bpf_ops_name = p;
+		if (!e->bpf_ops_name[0] ||
+		    strlen(e->bpf_ops_name) >= BINFMT_MISC_OPS_NAME_MAX)
+			goto einval;
+		p = s;
+		pr_debug("register: bpf handler: {%s}\n", e->bpf_ops_name);
+
+		/* The 'magic' field must be empty. */
+		s = strchr(p, del);
+		if (!s || s != p)
+			goto einval;
+		*s++ = '\0';
+		p = s;
+
+		/* The 'mask' field must be empty. */
+		s = strchr(p, del);
+		if (!s || s != p)
+			goto einval;
+		*s++ = '\0';
+		p = s;
+	} else if (test_bit(Magic, &e->flags)) {
 		/* Handle the 'M' (magic) format. */
 		char *s;
 
@@ -524,8 +611,13 @@ static Node *create_entry(const char __user *buffer, size_t count)
 	if (!p)
 		goto einval;
 	*p++ = '\0';
-	if (!e->interpreter[0])
+	if (test_bit(Bpf, &e->flags)) {
+		/* The program selects the interpreter at exec time. */
+		if (e->interpreter[0])
+			goto einval;
+	} else if (!e->interpreter[0]) {
 		goto einval;
+	}
 	pr_debug("register: interpreter: {%s}\n", e->interpreter);
 
 	/* Parse the 'flags' field. */
@@ -535,6 +627,14 @@ static Node *create_entry(const char __user *buffer, size_t count)
 	if (p != buf + count)
 		goto einval;
 
+	/*
+	 * A program-selected interpreter cannot be pre-opened and must not
+	 * inherit the credentials of a setuid binary it was chosen for.
+	 */
+	if (test_bit(Bpf, &e->flags) &&
+	    (e->flags & (MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
+		goto einval;
+
 	return e;
 
 out:
@@ -588,7 +688,10 @@ static void entry_status(Node *e, char *page)
 		return;
 	}
 
-	dp += sprintf(dp, "%s\ninterpreter %s\n", status, e->interpreter);
+	if (test_bit(Bpf, &e->flags))
+		dp += sprintf(dp, "%s\nbpf %s\n", status, e->bpf_ops_name);
+	else
+		dp += sprintf(dp, "%s\ninterpreter %s\n", status, e->interpreter);
 
 	/* print the special flags */
 	dp += sprintf(dp, "flags: ");
@@ -602,7 +705,9 @@ static void entry_status(Node *e, char *page)
 		*dp++ = 'F';
 	*dp++ = '\n';
 
-	if (!test_bit(Magic, &e->flags)) {
+	if (test_bit(Bpf, &e->flags)) {
+		*dp = '\0';
+	} else if (!test_bit(Magic, &e->flags)) {
 		sprintf(dp, "extension .%s\n", e->magic);
 	} else {
 		dp += sprintf(dp, "offset %i\nmagic ", e->offset);
@@ -809,6 +914,16 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer,
 	if (IS_ERR(e))
 		return PTR_ERR(e);
 
+	if (test_bit(Bpf, &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);
+			kfree(e);
+			return -ENOENT;
+		}
+	}
+
 	if (e->flags & MISC_FMT_OPEN_FILE) {
 		/*
 		 * Now that we support unprivileged binfmt_misc mounts make
@@ -834,6 +949,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);
 		kfree(e);
 		return err;
 	}

-- 
2.53.0


  parent reply	other threads:[~2026-07-07 19:37 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-07 19:36 [PATCH RFC POC 0/4] binfmt_misc: bpf-backed binary type handlers Christian Brauner
2026-07-07 19:36 ` [PATCH RFC POC 1/4] exec: stash a bpf-selected interpreter in struct linux_binprm Christian Brauner
2026-07-07 19:36 ` [PATCH RFC POC 2/4] binfmt_misc: add binfmt_misc_ops bpf struct_ops Christian Brauner
2026-07-07 19:36 ` Christian Brauner [this message]
2026-07-07 19:36 ` [PATCH RFC POC 4/4] bpf: allow fs kfuncs for binfmt_misc_ops programs Christian Brauner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260707-work-bpf-binfmt_misc-v1-3-74b995c84ec1@kernel.org \
    --to=brauner@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=corbet@lwn.net \
    --cc=daniel@iogearbox.net \
    --cc=farid.m.zakaria@gmail.com \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=kees@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=mail@johnericson.me \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox