From: Christian Brauner <[email protected]>
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/[email protected]
Signed-off-by: Christian Brauner (Amutable) <[email protected]>
Co-developed-by: Farid Zakaria <[email protected]>
Signed-off-by: Farid Zakaria <[email protected]>
Assisted-by: Claude:Opus-4.8
---
Documentation/admin-guide/binfmt-misc.rst | 40 +++++++-
fs/binfmt_misc.c | 149 +++++++++++++++++++++++++++---
2 files changed, 175 insertions(+), 14 deletions(-)
diff --git a/Documentation/admin-guide/binfmt-misc.rst
b/Documentation/admin-guide/binfmt-misc.rst
index 306ef48f5..de948dae7 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 fcaad14f8..4ece75f95 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -11,6 +11,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/binfmts.h>
+#include <linux/binfmt_misc.h>
#include <linux/bitops.h>
#include <linux/bits.h>
#include <linux/bug.h>
@@ -38,6 +39,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 +61,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 */
@@ -109,16 +113,21 @@ static bool entry_matches_extension(const struct
binfmt_misc_entry *e,
* 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 resumes the search after
+ * the first @bpf_skip candidates declined.
*
* The caller must hold the RCU read lock.
*
* Return: 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)
+search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm,
+ unsigned int bpf_skip)
{
char *dot = strrchr(bprm->interp, '.');
const char *ext = dot ? dot + 1 : NULL;
@@ -130,6 +139,15 @@ search_binfmt_handler(struct binfmt_misc *misc, struct
linux_binprm *bprm)
if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags))
continue;
+ /* A bpf handler is decided in load_misc_binary(). */
+ if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
+ if (bpf_skip) {
+ bpf_skip--;
+ continue;
+ }
+ return e;
+ }
+
if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) {
if (entry_matches_magic(e, bprm))
return e;
@@ -146,6 +164,7 @@ search_binfmt_handler(struct binfmt_misc *misc, struct
linux_binprm *bprm)
* 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(). The
@@ -156,13 +175,14 @@ search_binfmt_handler(struct binfmt_misc *misc, struct
linux_binprm *bprm)
* 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 linux_binprm *bprm,
+ unsigned int bpf_skip)
{
struct binfmt_misc_entry *e;
guard(rcu)();
do {
- e = search_binfmt_handler(misc, bprm);
+ e = search_binfmt_handler(misc, bprm, bpf_skip);
} while (e && !refcount_inc_not_zero(&e->users));
return e;
}
@@ -182,6 +202,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);
}
+ if (e->bpf_ops)
+ binfmt_misc_put_ops(e->bpf_ops);
/* Lockless walkers may still dereference this entry. */
kfree_rcu(e, rcu);
}
@@ -224,13 +246,16 @@ static int load_misc_binary(struct linux_binprm *bprm)
struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL;
struct file *interp_file;
struct binfmt_misc *misc;
+ const char *interpreter;
+ unsigned int bpf_skip = 0;
int retval;
misc = current_binfmt_misc();
if (!READ_ONCE(misc->enabled))
return -ENOEXEC;
- fmt = get_binfmt_handler(misc, bprm);
+retry:
+ fmt = get_binfmt_handler(misc, bprm, bpf_skip);
if (!fmt)
return -ENOEXEC;
@@ -238,6 +263,37 @@ static int load_misc_binary(struct linux_binprm *bprm)
if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
return -ENOENT;
+ if (test_bit(MISC_FMT_BPF_BIT, &fmt->flags)) {
+ /*
+ * A bpf-backed handler matches and picks the interpreter in
+ * sleepable context: > 0 means it handled the binary, 0 falls
+ * through to the handlers after this one and a negative errno
+ * fails the exec.
+ */
+ retval = fmt->bpf_ops->load(bprm);
+ if (retval < 0) {
+ /* Keep a program-supplied error within errno range. */
+ if (retval < -MAX_ERRNO)
+ retval = -ENOEXEC;
+ return retval;
+ }
+ if (!retval) {
+ /* Declined: move on to the handlers after this one. */
+ kfree(bprm->bpf_interp);
+ bprm->bpf_interp = NULL;
+ put_binfmt_handler(fmt);
+ fmt = NULL;
+ bpf_skip++;
+ goto retry;
+ }
+ /* Selecting an interpreter is part of the contract. */
+ if (!bprm->bpf_interp)
+ return -ENOEXEC;
+ interpreter = bprm->bpf_interp;
+ } else {
+ interpreter = fmt->interpreter;
+ }
+
if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) {
bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
} else {
@@ -256,13 +312,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;
@@ -277,7 +333,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);
@@ -428,6 +484,38 @@ static char *parse_extension_fields(struct
binfmt_misc_entry *e, char *p,
return p;
}
+/* Parse the 'offset' field of a 'B' entry: the bpf handler name. */
+static char *parse_bpf_fields(struct binfmt_misc_entry *e, char *p, char del)
+{
+ char *s;
+
+ /* The 'offset' field carries the bpf handler name. */
+ s = strchr(p, del);
+ if (!s)
+ return NULL;
+ *s++ = '\0';
+ e->bpf_ops_name = p;
+ if (!e->bpf_ops_name[0] ||
+ strlen(e->bpf_ops_name) >= BINFMT_MISC_OPS_NAME_MAX)
+ return NULL;
+ 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)
+ return NULL;
+ *s++ = '\0';
+ p = s;
+
+ /* The 'mask' field must be empty. */
+ s = strchr(p, del);
+ if (!s || s != p)
+ return NULL;
+ *s++ = '\0';
+ return s;
+}
+
/*
* This registers a new binary format, it recognises the syntax
* ':name:type:offset:magic:mask:interpreter:flags'
@@ -492,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':
+ if (!IS_ENABLED(CONFIG_BINFMT_MISC_BPF))
+ return ERR_PTR(-EINVAL);
+ pr_debug("register: type: B (bpf)\n");
+ 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);
@@ -511,8 +607,13 @@ 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 program selects the interpreter at exec time. */
+ if (e->interpreter[0])
+ return ERR_PTR(-EINVAL);
+ } else if (!e->interpreter[0]) {
return ERR_PTR(-EINVAL);
+ }
pr_debug("register: interpreter: {%s}\n", e->interpreter);
/* Parse the 'flags' field. */
@@ -522,6 +623,14 @@ static struct binfmt_misc_entry *create_entry(const char
__user *buffer,
if (p != buf + count)
return ERR_PTR(-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(MISC_FMT_BPF_BIT, &e->flags) &&
+ (e->flags & (MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
+ return ERR_PTR(-EINVAL);
+
return no_free_ptr(e);
}
@@ -575,7 +684,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: ");
@@ -589,7 +701,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)) {
+ /* No magic or extension to print for a bpf handler. */
+ } 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);
@@ -840,6 +954,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
@@ -864,6 +987,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.51.2