* [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry
@ 2026-07-30 13:34 Christian Brauner
2026-07-30 13:34 ` [PATCH 1/9] binfmt_misc: let a register string create an entry disabled Christian Brauner
` (8 more replies)
0 siblings, 9 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
A 'B' entry's load program hands the kernel an absolute path and
open_exec() resolves it at exec time in the mount namespace of whoever
runs the binary. So the handler names an interpreter but never gets to
say which file that is. Whoever controls the filesystem view of the exec
does.
Static entries have had the answer for a while. 'F' opens the file at
registration and every exec runs a clone of it. I can't just reuse it as
it stands. It pre-opens the one interpreter named in the register string
and a 'B' entry has no fixed interpreter. The program picks per exec,
and a qemu-user shaped handler wants one per guest architecture. So it
may want a whole set of them and that doesn't fit in a register string.
An entry is matchable the moment it is registered, so everything it
needs has to fit in that one write. Patch 1 adds a 'D' flag that creates
the entry disabled and splits a registration into create and activate:
echo ':qemu:B::::qemu_user:D' > register
echo '+aarch64 /usr/bin/qemu-aarch64' > qemu
echo '+arm /usr/bin/qemu-arm' > qemu
echo 1 > qemu
Each path is opened by its write, with the credentials the entry file was
opened with. Same open_exec() call, same place as 'F'. The program picks
one per exec with bpf_binprm_select_interp() and gets a clone of the
file. Nothing is resolved again, in any namespace.
A 'D' entry simply isn't hashed until that first '1', so the rcu
insertion that publishes the entry also publishes its interpreters and
the exec side needs no barriers. Reading the entry file doesn't take any
locks either. Bindings are rcu-published and the open file already pins
everything the read looks at. We use paths, not fds which makes the
config remain nice and static and can be shipped via /etc/binfmt.d.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Christian Brauner (9):
binfmt_misc: let a register string create an entry disabled
selftests/exec: let binfmt_flag_supported() return a bool
selftests/exec: test registering an entry disabled
binfmt_misc: document registering an entry disabled
selftests/exec: share the bpf handler preconditions
binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp
binfmt_misc: let a 'B' entry bind its interpreters
selftests/exec: test interpreters bound to a 'B' entry
binfmt_misc: document interpreters bound by a 'B' entry
Documentation/admin-guide/binfmt-misc.rst | 75 +++-
fs/binfmt_misc.c | 459 +++++++++++++++++----
fs/binfmt_misc_bpf.c | 75 +++-
fs/exec.c | 2 +
include/linux/binfmt_misc.h | 39 +-
include/linux/binfmts.h | 3 +
tools/testing/selftests/exec/Makefile | 11 +-
tools/testing/selftests/exec/binfmt_bind_interp.c | 14 +
tools/testing/selftests/exec/binfmt_misc_bpf.c | 306 ++++++++++++--
tools/testing/selftests/exec/binfmt_misc_common.h | 45 +-
.../testing/selftests/exec/binfmt_misc_disabled.c | 172 ++++++++
.../selftests/exec/binfmt_misc_transparent.c | 2 +-
tools/testing/selftests/exec/interp_bind.bpf.c | 76 ++++
13 files changed, 1164 insertions(+), 115 deletions(-)
---
base-commit: 4bdcf682a476e8d9f52b2c5c01e998d70e45656c
change-id: 20260729-work-binfmt_misc-preopen-9653fb9d1d04
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 1/9] binfmt_misc: let a register string create an entry disabled
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 2/9] selftests/exec: let binfmt_flag_supported() return a bool Christian Brauner
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
An entry is matchable as soon as it is registered. create_entry() sets
the enabled bit for every type and add_entry() links it straight into the
instance, so everything an entry needs has to fit in the write that
creates it.
Add a 'D' flag. The entry is created disabled and has to be enabled by
writing '1' to its entry file before it can match anything. That splits a
registration into create and activate, which a later patch uses to
configure an entry beyond what one register string can carry. It is
useful on its own too. Entries can be staged without dispatching the
moment they are written.
A staged entry stays out of the search list entirely. add_entry() only
hashes an entry that is born matchable, and the first '1' written to
the entry file hashes a staged one, which takes its place in the search
order at that point. The rcu insertion publishes the fully configured
entry, so the exec side keeps the plain enabled test it always had.
Removal cannot rely on the search list anymore. Whether an entry was
already removed is now decided by its dentry, '-1' to the status file
walks the directory instead of the list so staged entries do not
survive it, and a '1' through a file handle held across a removal
publishes nothing.
'D' is consumed at registration and not recorded. What matters afterwards
is whether the entry is enabled, and the entry file already reports that.
A 'B' entry's flags field had to be empty so far because every flag it
could name shaped the invocation, which a bpf handler picks per exec with
bpf_binprm_set_flags(). 'D' shapes the registration instead. So the rule
becomes what it always meant: a 'B' entry carries no invocation flags,
and 'D' composes.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/binfmt_misc.c | 98 ++++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 81 insertions(+), 17 deletions(-)
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 707f8a14f8a6..ca7840b01a2b 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -37,6 +37,8 @@
#include <linux/uaccess.h>
#include <linux/user_namespace.h>
+#include "internal.h"
+
/* Entry status and match type bit numbers. */
enum binfmt_misc_entry_bits {
MISC_FMT_ENABLED_BIT = 0,
@@ -52,8 +54,17 @@ enum binfmt_misc_entry_flags {
MISC_FMT_OPEN_FILE = (1U << 28),
MISC_FMT_TRANSPARENT = (1U << 27),
MISC_FMT_LOADER = (1U << 26),
+ MISC_FMT_DISABLED = (1U << 25),
};
+/* The flags that shape the invocation; a 'B' handler picks those per exec. */
+#define MISC_FMT_INVOCATION_FLAGS (MISC_FMT_PRESERVE_ARGV0 | \
+ MISC_FMT_OPEN_BINARY | \
+ MISC_FMT_CREDENTIALS | \
+ MISC_FMT_OPEN_FILE | \
+ MISC_FMT_TRANSPARENT | \
+ MISC_FMT_LOADER)
+
/**
* struct binfmt_misc_flag - a flag character of the register string
* @c: the character userspace writes and reads back
@@ -75,6 +86,7 @@ static const struct binfmt_misc_flag misc_flags[] = {
{ 'F', MISC_FMT_OPEN_FILE, 0, "open interpreter file now" },
{ 'T', MISC_FMT_TRANSPARENT, MISC_FMT_OPEN_BINARY, "transparent" },
{ 'L', MISC_FMT_LOADER, 0, "loader substitution" },
+ { 'D', MISC_FMT_DISABLED, 0, "register disabled" },
};
/* Look up a flag character, NULL if @c is not one. */
@@ -175,7 +187,12 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm)
/* Walk all the registered handlers. */
hlist_for_each_entry_rcu(e, &misc->entries, node,
srcu_read_lock_held(&bm_entries_srcu)) {
- /* Make sure this one is currently enabled. */
+ /*
+ * Make sure this one is currently enabled. An entry enters
+ * the list at most once and only whole: its configuration is
+ * ordered before the rcu insertion that makes it visible
+ * here.
+ */
if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags))
continue;
@@ -684,7 +701,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
size_t count)
{
struct binfmt_misc_entry *e __free(kfree) = NULL;
- char *buf, *p, *flags;
+ char *buf, *p;
char del;
pr_debug("register: received %zu bytes\n", count);
@@ -780,18 +797,29 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
}
/* Parse the 'flags' field. */
- flags = p;
p = check_special_flags(p, e);
/*
* A bpf handler decides the invocation flags per exec with
* bpf_binprm_set_flags() rather than fixing them at registration, and
* 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B'
- * entry's flags field has to be empty.
+ * entry carries no invocation flags.
*/
- if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags)
+ if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
+ (e->flags & MISC_FMT_INVOCATION_FLAGS))
return ERR_PTR(-EINVAL);
+ /*
+ * 'D' is a directive for this registration rather than a lasting
+ * property, so consume it: the entry is created disabled and stays
+ * out of the search list until '1' is written to its entry file.
+ * The first enable publishes it, for good.
+ */
+ if (e->flags & MISC_FMT_DISABLED) {
+ e->flags &= ~MISC_FMT_DISABLED;
+ clear_bit(MISC_FMT_ENABLED_BIT, &e->flags);
+ }
+
/* Transparency preserves the whole argv, argv[0] included. */
if ((e->flags & MISC_FMT_TRANSPARENT) &&
(e->flags & MISC_FMT_PRESERVE_ARGV0))
@@ -852,6 +880,12 @@ static int parse_command(const char __user *buffer, size_t count)
/* generic stuff */
+/* The root directory's inode; its lock serializes configuring an instance. */
+static struct inode *bm_root_inode(struct super_block *sb)
+{
+ return d_inode(sb->s_root);
+}
+
static void bm_seq_hex(struct seq_file *m, const u8 *data, int size)
{
for (int i = 0; i < size; i++)
@@ -992,10 +1026,11 @@ static void remove_binfmt_handler(struct binfmt_misc *misc,
/* Remove @e unless it was already removed. */
static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb)
{
- struct inode *root = d_inode(sb->s_root);
+ struct inode *root = bm_root_inode(sb);
inode_lock_nested(root, I_MUTEX_PARENT);
- if (!hlist_unhashed(&e->node))
+ /* A staged entry is not hashed; the dentry says if it was removed. */
+ if (!d_unhashed(e->dentry))
remove_binfmt_handler(i_binfmt_misc(root), e);
inode_unlock(root);
}
@@ -1004,13 +1039,21 @@ static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb)
static void bm_remove_all_entries(struct binfmt_misc *misc,
struct super_block *sb)
{
- struct inode *root = d_inode(sb->s_root);
- struct binfmt_misc_entry *e;
- struct hlist_node *next;
+ struct inode *root = bm_root_inode(sb);
+ struct dentry *child = NULL;
inode_lock_nested(root, I_MUTEX_PARENT);
- hlist_for_each_entry_safe(e, next, &misc->entries, node)
- remove_binfmt_handler(misc, e);
+ /*
+ * Walk the directory rather than the search list: a staged entry
+ * is in the former but not yet in the latter. The control files
+ * carry no entry and stay.
+ */
+ while ((child = find_next_child(sb->s_root, child))) {
+ struct binfmt_misc_entry *e = d_inode(child)->i_private;
+
+ if (e)
+ remove_binfmt_handler(misc, e);
+ }
inode_unlock(root);
}
@@ -1067,9 +1110,27 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer,
case BM_CMD_DISABLE:
clear_bit(MISC_FMT_ENABLED_BIT, &e->flags);
break;
- case BM_CMD_ENABLE:
+ case BM_CMD_ENABLE: {
+ struct inode *root = bm_root_inode(inode->i_sb);
+
+ /*
+ * The first enable publishes a 'D' entry into the search
+ * list, whole. The lock keeps that ordered against a second
+ * enable and against removal; a removed entry has nothing
+ * left to publish.
+ */
+ inode_lock(root);
set_bit(MISC_FMT_ENABLED_BIT, &e->flags);
+ if (hlist_unhashed(&e->node) && !d_unhashed(e->dentry)) {
+ struct binfmt_misc *misc = i_binfmt_misc(inode);
+
+ spin_lock(&misc->entries_lock);
+ hlist_add_head_rcu(&e->node, &misc->entries);
+ spin_unlock(&misc->entries_lock);
+ }
+ inode_unlock(root);
break;
+ }
case BM_CMD_REMOVE:
bm_remove_entry(e, inode->i_sb);
break;
@@ -1112,10 +1173,13 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb)
inode->i_fop = &bm_entry_operations;
d_make_persistent(dentry, inode);
- misc = i_binfmt_misc(inode);
- spin_lock(&misc->entries_lock);
- hlist_add_head_rcu(&e->node, &misc->entries);
- spin_unlock(&misc->entries_lock);
+ /* A 'D' entry stays out of the search list until its first enable. */
+ if (test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) {
+ misc = i_binfmt_misc(inode);
+ spin_lock(&misc->entries_lock);
+ hlist_add_head_rcu(&e->node, &misc->entries);
+ spin_unlock(&misc->entries_lock);
+ }
simple_done_creating(dentry);
return 0;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 2/9] selftests/exec: let binfmt_flag_supported() return a bool
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
2026-07-30 13:34 ` [PATCH 1/9] binfmt_misc: let a register string create an entry disabled Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 3/9] selftests/exec: test registering an entry disabled Christian Brauner
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
binfmt_flag_supported() returns 0 when the flag is supported and -1
when it is not, so every caller reads backwards:
if (binfmt_flag_supported('T'))
SKIP(return, "kernel without the 'T' flag");
Make it return a bool and flip the callers. errno from a failed probe
is still set for callers that check it.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/exec/binfmt_misc_bpf.c | 2 +-
tools/testing/selftests/exec/binfmt_misc_common.h | 6 +++---
tools/testing/selftests/exec/binfmt_misc_transparent.c | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index 069768a66ba0..c6f5e8f34985 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -258,7 +258,7 @@ TEST_F(bpf_handler, transparent_dispatch)
char src[PATH_MAX], cmd[PATH_MAX + 16];
/* Probe for transparent-mode support via its static counterpart. */
- if (binfmt_flag_supported('T'))
+ if (!binfmt_flag_supported('T'))
SKIP(return, "kernel without transparent mode");
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0);
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
index c6900ded019f..e8d67908dbc4 100644
--- a/tools/testing/selftests/exec/binfmt_misc_common.h
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -117,16 +117,16 @@ static inline int artifact_path(char *out, size_t sz, const char *name)
}
/* Probe kernel support for a registration flag with a throwaway entry. */
-static inline int binfmt_flag_supported(char flag)
+static inline bool binfmt_flag_supported(char flag)
{
char rule[64];
snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c",
flag);
if (write_reg(rule))
- return -1;
+ return false;
unregister("bm_flag_probe");
- return 0;
+ return true;
}
/*
diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c
index d0cb845df1d3..2ebf73de8018 100644
--- a/tools/testing/selftests/exec/binfmt_misc_transparent.c
+++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c
@@ -56,7 +56,7 @@ FIXTURE_SETUP(transparent)
ASSERT_EQ(create_target(), 0);
/* Skip the whole suite on a kernel that does not know 'T'. */
- if (binfmt_flag_supported('T')) {
+ if (!binfmt_flag_supported('T')) {
ASSERT_EQ(errno, EINVAL);
SKIP(return, "kernel without the 'T' flag");
}
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 3/9] selftests/exec: test registering an entry disabled
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
2026-07-30 13:34 ` [PATCH 1/9] binfmt_misc: let a register string create an entry disabled Christian Brauner
2026-07-30 13:34 ` [PATCH 2/9] selftests/exec: let binfmt_flag_supported() return a bool Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 4/9] binfmt_misc: document " Christian Brauner
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
A magic entry registered with 'D' and the same entry without it, to pin
down what the flag decides and what it leaves alone:
- the entry reports itself disabled and nothing dispatches until '1' is
written to it
- without 'D' it dispatches straight away
- 'D' is not read back among the entry's flags
- enabling and disabling afterwards works as it does for any entry
- 'D' composes with the flags that shape the invocation
- '-1' to the status file removes a staged entry like any other
- a file handle held across a removal cannot resurrect the entry
Put the entry write and read-back helpers into binfmt_misc_common.h.
The bpf suite will need them as well.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/exec/Makefile | 4 +
tools/testing/selftests/exec/binfmt_misc_common.h | 39 +++++
.../testing/selftests/exec/binfmt_misc_disabled.c | 172 +++++++++++++++++++++
3 files changed, 215 insertions(+)
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 390fe11a7bed..ec7894a802e0 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -25,6 +25,10 @@ TEST_GEN_PROGS += check-exec
# or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
TEST_GEN_PROGS += binfmt_misc_selfpin
+# 'D' (register disabled) binfmt_misc test: an entry that exists but does
+# not dispatch until it is enabled. Static magic entry, no bpf toolchain.
+TEST_GEN_PROGS += binfmt_misc_disabled
+
# Static ('T' flag) transparent binfmt_misc test; the asserting interpreter
# is shared with the bpf harness's transparent case. No bpf toolchain needed.
TEST_GEN_PROGS += binfmt_misc_transparent
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
index e8d67908dbc4..745aff84dc78 100644
--- a/tools/testing/selftests/exec/binfmt_misc_common.h
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -93,6 +93,45 @@ static inline void unregister(const char *name)
}
}
+/* Write @line to @entry's file, reporting the errno it was refused with. */
+static inline int entry_command(const char *entry, const char *line)
+{
+ char path[PATH_MAX];
+ int fd, retval = 0;
+ size_t len = strlen(line);
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+ if (write(fd, line, len) != (ssize_t)len)
+ retval = -errno;
+ close(fd);
+ return retval;
+}
+
+/* Does @entry's file report @line? */
+static inline bool entry_shows(const char *entry, const char *line)
+{
+ char path[PATH_MAX], buf[PATH_MAX];
+ bool found = false;
+ FILE *fp;
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+ fp = fopen(path, "r");
+ if (!fp)
+ return false;
+ while (fgets(buf, sizeof(buf), fp)) {
+ buf[strcspn(buf, "\n")] = '\0';
+ if (!strcmp(buf, line)) {
+ found = true;
+ break;
+ }
+ }
+ fclose(fp);
+ return found;
+}
+
/* Mount binfmt_misc unless it already is, and report whether it is usable. */
static inline bool binfmt_misc_available(void)
{
diff --git a/tools/testing/selftests/exec/binfmt_misc_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c
new file mode 100644
index 000000000000..47c9e8a4ee42
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_disabled.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the 'D' (register disabled) flag of binfmt_misc. An entry
+ * registered with it exists but cannot be matched until userspace enables
+ * it, which splits a registration into create and activate.
+ *
+ * Needs root for the registration; no bpf toolchain involved.
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define MAGIC "#DISABLED-SELFTEST#"
+#define TARGET_PATH "/tmp/binfmt_disabled_target"
+#define INTERP_PATH "/tmp/binfmt_disabled_interp.sh"
+#define ENTRY "test_disabled"
+#define RULE(flags) ":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags
+
+/* The interpreter exits with a code the harness can recognise. */
+#define EXIT_INTERP 7
+
+/* The target only has to carry the magic; it is never actually loaded. */
+static int create_target(void)
+{
+ char buf[128] = MAGIC "\n";
+ int fd;
+
+ unlink(TARGET_PATH);
+ fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ return 0;
+}
+
+static int create_interp(void)
+{
+ char buf[64];
+ int fd;
+
+ unlink(INTERP_PATH);
+ fd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ snprintf(buf, sizeof(buf), "#!/bin/sh\nexit %d\n", EXIT_INTERP);
+ if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
+ close(fd);
+ return -1;
+ }
+ return close(fd);
+}
+
+FIXTURE(disabled) {
+};
+
+FIXTURE_SETUP(disabled)
+{
+ if (getuid() != 0)
+ SKIP(return, "test must be run as root");
+ if (!binfmt_misc_available())
+ SKIP(return, "no binfmt_misc");
+
+ /* Skip the whole suite on a kernel that does not know 'D'. */
+ if (!binfmt_flag_supported('D')) {
+ ASSERT_EQ(errno, EINVAL);
+ SKIP(return, "kernel without the 'D' flag");
+ }
+
+ ASSERT_EQ(create_interp(), 0);
+ ASSERT_EQ(create_target(), 0);
+}
+
+FIXTURE_TEARDOWN(disabled)
+{
+ unregister(ENTRY);
+ unlink(TARGET_PATH);
+ unlink(INTERP_PATH);
+}
+
+/* The entry exists but does not dispatch until it is enabled. */
+TEST_F(disabled, inert_until_enabled)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+ /* Nothing matches it, so no binary format claims the target. */
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* Without 'D' an entry is matchable the moment it is registered. */
+TEST_F(disabled, enabled_without_the_flag)
+{
+ ASSERT_EQ(write_reg(RULE("")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' is spent on the registration: the entry does not report it back. */
+TEST_F(disabled, flag_not_reported)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_FALSE(entry_shows(ENTRY, "flags: D"));
+ EXPECT_TRUE(entry_shows(ENTRY, "flags: "));
+}
+
+/* A disabled entry can be disabled and enabled like any other. */
+TEST_F(disabled, toggles_like_any_entry)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ ASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+ ASSERT_EQ(entry_command(ENTRY, "0\n"), 0);
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' composes with the invocation flags a static entry can carry. */
+TEST_F(disabled, composes_with_invocation_flags)
+{
+ ASSERT_EQ(write_reg(RULE("PD")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+ EXPECT_TRUE(entry_shows(ENTRY, "flags: P"));
+}
+
+/* '-1' to the status file sweeps a staged entry with everything else. */
+TEST_F(disabled, removed_by_remove_all)
+{
+ int fd;
+
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+ fd = open(BINFMT_DIR "/status", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, "-1", 2), 2);
+ close(fd);
+
+ EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+}
+
+/* A file handle held across a removal cannot resurrect the entry. */
+TEST_F(disabled, no_resurrection_after_remove)
+{
+ int fd;
+
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ fd = open(BINFMT_DIR "/" ENTRY, O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(write(fd, "-1", 2), 2);
+ EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+
+ /* Accepted like any toggle of a removed entry, but publishes nothing. */
+ EXPECT_EQ(write(fd, "1", 1), 1);
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 4/9] binfmt_misc: document registering an entry disabled
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
` (2 preceding siblings ...)
2026-07-30 13:34 ` [PATCH 3/9] selftests/exec: test registering an entry disabled Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 5/9] selftests/exec: share the bpf handler preconditions Christian Brauner
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
Describe the 'D' flag and what it changes about a registration:
- that the entry has to be enabled before it dispatches anything
- and that the flag is not read back
Scope the bpf section's "carries no flags" rule to invocation flags now
that 'D' composes with 'B'.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Documentation/admin-guide/binfmt-misc.rst | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index c80702b4ccd5..8254ddcb3389 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -107,6 +107,15 @@ Here is what the fields mean:
``PT_INTERP``. See the "Loader substitution" section
below. ``L`` rejects ``T``, ``P``, ``O`` and ``C``;
``F`` composes.
+ ``D`` - registered disabled
+ The entry is created disabled instead of being matchable at
+ once, and has to be enabled by writing ``1`` to its file
+ before it dispatches anything. This splits a registration
+ into creating the entry and activating it, leaving room to
+ configure it in between - which is what a ``B`` entry that
+ binds interpreters needs; see the bpf section below. The flag
+ is spent on the registration and is not read back: what an
+ entry file reports afterwards is whether it is enabled.
There are some restrictions:
@@ -224,8 +233,10 @@ handler can decide them differently for each binary it handles:
``PT_INTERP`` and runs the binary as a fully native exec (the ``L``
flag). It excludes the other flags and a staged interpreter argument.
-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.
+Because these are program choices, a ``B`` entry carries no invocation
+flags in the register string; ``F`` (pre-open a fixed interpreter) has no
+meaning for it. The registration directive ``D`` is the exception: it
+decides how the entry starts out, not how the interpreter is invoked.
Handlers are looked up in the user namespace the struct_ops map was
registered in, falling back to ancestor namespaces, mirroring how
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 5/9] selftests/exec: share the bpf handler preconditions
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
` (3 preceding siblings ...)
2026-07-30 13:34 ` [PATCH 4/9] binfmt_misc: document " Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 6/9] binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp Christian Brauner
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
The bpf handler fixture opens with three probes, each with its own SKIP.
More fixtures with the same needs are about to be added, so hoist the
probes into a helper that reports the first missing precondition.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/exec/binfmt_misc_bpf.c | 43 ++++++++++++++++----------
1 file changed, 27 insertions(+), 16 deletions(-)
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index c6f5e8f34985..71bb6d8b4517 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -106,6 +106,30 @@ static int check_output(const char *cmd, const char *expected)
return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
}
+/* Does the kernel BTF know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF)? */
+static bool have_binfmt_misc_ops(void)
+{
+ struct btf *btf = btf__load_vmlinux_btf();
+ bool have;
+
+ have = btf && btf__find_by_name_kind(btf, "binfmt_misc_ops",
+ BTF_KIND_STRUCT) >= 0;
+ btf__free(btf);
+ return have;
+}
+
+/* The reason bpf handler cases cannot run here, NULL if they can. */
+static const char *bpf_handler_unsupported(void)
+{
+ if (getuid() != 0)
+ return "test must be run as root";
+ if (!have_binfmt_misc_ops())
+ return "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)";
+ if (!binfmt_misc_available())
+ return "no binfmt_misc";
+ return NULL;
+}
+
/* An attached handler with its 'B' entry activated. */
struct bpf_case {
struct bpf_object *obj;
@@ -190,23 +214,10 @@ FIXTURE(bpf_handler) {
FIXTURE_SETUP(bpf_handler)
{
char src[PATH_MAX];
- struct btf *btf;
-
- if (getuid() != 0)
- SKIP(return, "test must be run as root");
+ const char *why = bpf_handler_unsupported();
- /* 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) {
- btf__free(btf);
- SKIP(return,
- "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)");
- }
- btf__free(btf);
-
- if (!binfmt_misc_available())
- SKIP(return, "no binfmt_misc");
+ if (why)
+ SKIP(return, "%s", why);
/* Shared test interpreter. */
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0);
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 6/9] binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
` (4 preceding siblings ...)
2026-07-30 13:34 ` [PATCH 5/9] selftests/exec: share the bpf handler preconditions Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 7/9] binfmt_misc: let a 'B' entry bind its interpreters Christian Brauner
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
An 'F' entry opens its interpreter at registration and every exec runs a
clone of that file. The file lives in a bare struct file pointer next to
the path it came from and put_binfmt_handler() closes it as a special
case.
Give the pre-opened interpreter a type of its own instead. struct
binfmt_misc_interp carries the file, the path it was opened from and a
selection name in a single allocation and is linked on a list that the
entry owns and tears down in put_binfmt_handler(). An 'F' entry binds a
single interpreter under the empty name and hands out clones of it as
before.
The open moves into open_interp_file() and works exactly as the
open-coded block in bm_register_write() did. It is opened for execution
at registration time, in the writer's context and with the credentials
the register file was opened with.
The entry can now own objects before it is published, so make
put_binfmt_handler() the single teardown. create_entry() returns the
entry with its reference held and every failure path in
bm_register_write() simply puts it. That also replaces the open-coded
bpf_ops release.
No functional changes. A later patch lets a 'B' entry bind multiple
interpreters selected by name per exec and reuses all of this.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/binfmt_misc.c | 151 +++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 119 insertions(+), 32 deletions(-)
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index ca7840b01a2b..afd8a737c95b 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -24,6 +24,7 @@
#include <linux/fs_context.h>
#include <linux/init.h>
#include <linux/kstrtox.h>
+#include <linux/list.h>
#include <linux/magic.h>
#include <linux/module.h>
#include <linux/printk.h>
@@ -98,6 +99,24 @@ static const struct binfmt_misc_flag *misc_flag_by_char(const char c)
return NULL;
}
+/**
+ * struct binfmt_misc_interp - an interpreter an entry was registered with
+ * @list: link in the entry's list, in registration order
+ * @file: the file, opened at registration and never resolved again
+ * @path: the path it was registered under, used as the name the interpreter
+ * runs under; stored after @name in the same allocation
+ * @name: the name a load program selects it by; empty for the fixed
+ * interpreter of a static 'F' entry
+ *
+ * Owned by the entry and living exactly as long as it does.
+ */
+struct binfmt_misc_interp {
+ struct list_head list;
+ struct file *file;
+ const char *path;
+ char name[];
+};
+
struct binfmt_misc_entry {
struct hlist_node node;
unsigned long flags; /* type, status, etc. */
@@ -108,9 +127,9 @@ struct binfmt_misc_entry {
const char *interpreter; /* filename of interpreter */
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;
+ struct list_head interps; /* the interpreters it bound */
refcount_t users; /* sync removal with load_misc_binary() */
struct rcu_head rcu;
char buf[]; /* register string, fields point in here */
@@ -233,6 +252,82 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc,
return search_binfmt_handler(misc, bprm);
}
+/* Undo the open_exec() a pre-opened interpreter file came from. */
+static void close_interp_file(struct file *f)
+{
+ if (IS_ERR_OR_NULL(f))
+ return;
+ exe_file_allow_write_access(f);
+ filp_close(f, NULL);
+}
+
+/*
+ * Open an interpreter @path for execution: now, in the writer's context,
+ * and - since binfmt_misc mounts can be unprivileged - with @cred, the
+ * credentials the control file being written was opened with, not the
+ * writer's own.
+ */
+static struct file *open_interp_file(const struct cred *cred, const char *path)
+{
+ struct file *f;
+
+ scoped_with_creds(cred)
+ f = open_exec(path);
+ if (IS_ERR(f))
+ pr_notice("register: failed to install interpreter %s\n", path);
+ return f;
+}
+
+/* Release the interpreters an entry was registered with. */
+static void entry_put_interpreters(struct binfmt_misc_entry *e)
+{
+ struct binfmt_misc_interp *interp, *tmp;
+
+ list_for_each_entry_safe(interp, tmp, &e->interps, list) {
+ list_del(&interp->list);
+ close_interp_file(interp->file);
+ kfree(interp);
+ }
+}
+
+/**
+ * entry_attach_interpreter - bind an opened interpreter to @e
+ * @e: entry being configured
+ * @name: name a load program can select it by; empty for the fixed
+ * interpreter of a static entry
+ * @path: the path @f was opened from
+ * @f: the interpreter, opened for execution
+ *
+ * Every exec runs a clone of @f, so the path decided which file is bound
+ * and nothing else: it is not resolved again, in any namespace.
+ *
+ * The caller has to have established that @e cannot be matched yet, and
+ * owns @f until this succeeds.
+ *
+ * Return: 0 on success, a negative errno on failure
+ */
+static int entry_attach_interpreter(struct binfmt_misc_entry *e,
+ const char *name, const char *path,
+ struct file *f)
+{
+ size_t nlen = strlen(name), plen = strlen(path);
+ struct binfmt_misc_interp *interp;
+
+ /* One allocation, both strings in it, like the entry's own buffer. */
+ interp = kmalloc(struct_size(interp, name, nlen + plen + 2),
+ GFP_KERNEL_ACCOUNT);
+ if (!interp)
+ return -ENOMEM;
+
+ interp->path = interp->name + nlen + 1;
+ strscpy(interp->name, name, nlen + 1);
+ strscpy(interp->name + nlen + 1, path, plen + 1);
+ interp->file = f;
+ list_add_tail(&interp->list, &e->interps);
+ pr_debug("register: interpreter: %s {%s}\n", name, path);
+ return 0;
+}
+
static void bm_entry_free_rcu(struct rcu_head *rcu)
{
struct binfmt_misc_entry *e = container_of(rcu, struct binfmt_misc_entry, rcu);
@@ -249,21 +344,22 @@ static void bm_entry_free_rcu(struct rcu_head *rcu)
*
* Free entry syncing with load_misc_binary() and defer final free to
* load_misc_binary() in case it is using the binary type handler we were
- * requested to remove.
+ * requested to remove. Also the teardown for a registration that fails
+ * before add_entry() publishes the entry.
*/
static void put_binfmt_handler(struct binfmt_misc_entry *e)
{
+ if (IS_ERR_OR_NULL(e))
+ return;
+
if (refcount_dec_and_test(&e->users)) {
- if (e->flags & MISC_FMT_OPEN_FILE) {
- exe_file_allow_write_access(e->interp_file);
- filp_close(e->interp_file, NULL);
- }
+ entry_put_interpreters(e);
/* Walkers may still dereference this entry, even sleeping. */
call_srcu(&bm_entries_srcu, &e->rcu, bm_entry_free_rcu);
}
}
-DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, if (_T) put_binfmt_handler(_T))
+DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, put_binfmt_handler(_T))
/**
* current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace
@@ -392,12 +488,15 @@ static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,
const char *interpreter)
{
struct file *interp_file __free(fput) = NULL;
+ struct binfmt_misc_interp *interp;
int retval;
if (!(e->flags & MISC_FMT_OPEN_FILE))
return open_exec(interpreter);
- interp_file = file_clone_open(e->interp_file);
+ /* An 'F' entry pre-opened exactly one interpreter. */
+ interp = list_first_entry(&e->interps, struct binfmt_misc_interp, list);
+ interp_file = file_clone_open(interp->file);
if (IS_ERR(interp_file))
return interp_file;
@@ -718,6 +817,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
p = buf = e->buf;
memset(e, 0, sizeof(*e));
+ INIT_LIST_HEAD(&e->interps);
if (copy_from_user(buf, buffer, count))
return ERR_PTR(-EFAULT);
@@ -842,6 +942,8 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
e->interpreter[0] != '/')
return ERR_PTR(-EINVAL);
+ /* Born holding one reference; put_binfmt_handler() is the teardown. */
+ refcount_set(&e->users, 1);
return no_free_ptr(e);
}
@@ -1167,7 +1269,6 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb)
return -ENOMEM;
}
- refcount_set(&e->users, 1);
e->dentry = dentry;
inode->i_private = e;
inode->i_fop = &bm_entry_operations;
@@ -1187,9 +1288,8 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb)
static ssize_t bm_register_write(struct file *file, const char __user *buffer,
size_t count, loff_t *ppos)
{
- struct binfmt_misc_entry *e __free(kfree) = NULL;
+ struct binfmt_misc_entry *e __free(put_binfmt_handler) = NULL;
struct super_block *sb = file_inode(file)->i_sb;
- struct file *f = NULL;
int err;
e = create_entry(buffer, count);
@@ -1206,33 +1306,20 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer,
}
if (e->flags & MISC_FMT_OPEN_FILE) {
- /*
- * Now that we support unprivileged binfmt_misc mounts make
- * sure we use the credentials that the register @file was
- * opened with to also open the interpreter. Before that this
- * didn't matter much as only a privileged process could open
- * the register file.
- */
- scoped_with_creds(file->f_cred)
- f = open_exec(e->interpreter);
- if (IS_ERR(f)) {
- pr_notice("register: failed to install interpreter file %s\n",
- e->interpreter);
+ struct file *f = open_interp_file(file->f_cred, e->interpreter);
+
+ if (IS_ERR(f))
return PTR_ERR(f);
+ err = entry_attach_interpreter(e, "", e->interpreter, f);
+ if (err) {
+ close_interp_file(f);
+ return err;
}
- e->interp_file = f;
}
err = add_entry(e, sb);
- if (err) {
- if (f) {
- exe_file_allow_write_access(f);
- filp_close(f, NULL);
- }
- if (e->bpf_ops)
- binfmt_misc_put_ops(e->bpf_ops);
+ if (err)
return err;
- }
/* The entry is owned by its inode now. */
retain_and_null_ptr(e);
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 7/9] binfmt_misc: let a 'B' entry bind its interpreters
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
` (5 preceding siblings ...)
2026-07-30 13:34 ` [PATCH 6/9] binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 8/9] selftests/exec: test interpreters bound to a 'B' entry Christian Brauner
2026-07-30 13:34 ` [PATCH 9/9] binfmt_misc: document interpreters bound by " Christian Brauner
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
A 'B' entry's load program selects its interpreter by absolute path,
which open_exec() resolves at exec time in the mount namespace of whoever
runs the binary. The handler names an interpreter but does not get to say
which file that is. Whoever controls the filesystem view of the exec
decides that instead.
Static entries settled this long ago with 'F'. The interpreter is opened
at registration in the registrant's context and every exec runs a clone
of that file. Give a 'B' entry the same, for as many interpreters as it
needs.
An entry registered with 'D' cannot be matched yet, so it still belongs
to whoever is configuring it and can be given interpreters one write at a
time:
echo ':qemu:B::::qemu_user:D' > register
echo '+aarch64 /usr/bin/qemu-aarch64' > qemu
echo '+arm /usr/bin/qemu-arm' > qemu
echo 1 > qemu
Each path is opened by its write, with the credentials the entry file
was opened with, by the same helper that opens an 'F' interpreter. The
load program picks one per exec with bpf_binprm_select_interp() and the
entry hands out a clone of it. Nothing is resolved again, in any
namespace. The path is everything past the first space, so no
interpreter has to fit in a register string. An entry binds at most a
hundred interpreters (BINFMT_MISC_INTERP_MAX). Every binding pins a
struct file that no file descriptor accounts for, so RLIMIT_NOFILE does
not apply and some cap is needed. A hundred is plenty and raising it
later is cheap, lowering it is not.
Selection is by name so the register string and the program need not
agree on an order, and so the handler is not tied to where a distribution
puts its interpreters. A name is a single word of printable ASCII so the
entry file can report 'name path' lines. The interpreter runs under the
path it was registered under.
The entry file reads user memory once. bm_entry_write() copies the write
in and dispatches on the first byte, and parse_command() takes the copied
buffer. The status file has no binding to spell, so it keeps its own
small copy in read_command().
That moves the length cap ahead of the dispatch. A write to an entry file
longer than a binding can be is now refused with -E2BIG, and one from a
bad address reports -EFAULT, where the command parser used to report
-EINVAL for anything past three bytes.
Configurations of one instance are kept apart by the lock removal
already takes. Reading the set out of the entry file takes no lock.
Bindings are rcu-published and the open entry file pins the entry
together with everything it bound, so a reader either sees a whole node
or misses it. The interpreter is opened before the configuration lock
because resolving the path may walk this very filesystem, and only
after the command has been parsed and the name validated from the
copied buffer, so a write that can never bind opens nothing and the
errno reflects the actual failure.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/binfmt_misc.c | 266 +++++++++++++++++++++++++++++++++++---------
fs/binfmt_misc_bpf.c | 75 ++++++++++++-
fs/exec.c | 2 +
include/linux/binfmt_misc.h | 39 ++++++-
include/linux/binfmts.h | 3 +
5 files changed, 326 insertions(+), 59 deletions(-)
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index afd8a737c95b..ad8c4f64bf10 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -24,6 +24,7 @@
#include <linux/fs_context.h>
#include <linux/init.h>
#include <linux/kstrtox.h>
+#include <linux/limits.h>
#include <linux/list.h>
#include <linux/magic.h>
#include <linux/module.h>
@@ -99,24 +100,6 @@ static const struct binfmt_misc_flag *misc_flag_by_char(const char c)
return NULL;
}
-/**
- * struct binfmt_misc_interp - an interpreter an entry was registered with
- * @list: link in the entry's list, in registration order
- * @file: the file, opened at registration and never resolved again
- * @path: the path it was registered under, used as the name the interpreter
- * runs under; stored after @name in the same allocation
- * @name: the name a load program selects it by; empty for the fixed
- * interpreter of a static 'F' entry
- *
- * Owned by the entry and living exactly as long as it does.
- */
-struct binfmt_misc_interp {
- struct list_head list;
- struct file *file;
- const char *path;
- char name[];
-};
-
struct binfmt_misc_entry {
struct hlist_node node;
unsigned long flags; /* type, status, etc. */
@@ -252,6 +235,24 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc,
return search_binfmt_handler(misc, bprm);
}
+/**
+ * binfmt_misc_find_interp - find a bound interpreter by name
+ * @interps: the interpreters the matched entry was registered with
+ * @name: the name to look for
+ *
+ * Return: the interpreter on success, NULL if @interps has none by that name
+ */
+const struct binfmt_misc_interp *
+binfmt_misc_find_interp(const struct list_head *interps, const char *name)
+{
+ struct binfmt_misc_interp *interp;
+
+ list_for_each_entry(interp, interps, list)
+ if (!strcmp(interp->name, name))
+ return interp;
+ return NULL;
+}
+
/* Undo the open_exec() a pre-opened interpreter file came from. */
static void close_interp_file(struct file *f)
{
@@ -261,6 +262,8 @@ static void close_interp_file(struct file *f)
filp_close(f, NULL);
}
+DEFINE_FREE(close_interp_file, struct file *, close_interp_file(_T))
+
/*
* Open an interpreter @path for execution: now, in the writer's context,
* and - since binfmt_misc mounts can be unprivileged - with @cred, the
@@ -293,7 +296,7 @@ static void entry_put_interpreters(struct binfmt_misc_entry *e)
/**
* entry_attach_interpreter - bind an opened interpreter to @e
* @e: entry being configured
- * @name: name a load program can select it by; empty for the fixed
+ * @name: name the load program will select it by; empty for the fixed
* interpreter of a static entry
* @path: the path @f was opened from
* @f: the interpreter, opened for execution
@@ -301,8 +304,8 @@ static void entry_put_interpreters(struct binfmt_misc_entry *e)
* Every exec runs a clone of @f, so the path decided which file is bound
* and nothing else: it is not resolved again, in any namespace.
*
- * The caller has to have established that @e cannot be matched yet, and
- * owns @f until this succeeds.
+ * The caller has to have validated @name and @path, established that @e
+ * cannot be matched yet, and owns @f until this succeeds.
*
* Return: 0 on success, a negative errno on failure
*/
@@ -313,6 +316,11 @@ static int entry_attach_interpreter(struct binfmt_misc_entry *e,
size_t nlen = strlen(name), plen = strlen(path);
struct binfmt_misc_interp *interp;
+ if (binfmt_misc_find_interp(&e->interps, name))
+ return -EEXIST;
+ if (list_count_nodes(&e->interps) >= BINFMT_MISC_INTERP_MAX)
+ return -ENOSPC;
+
/* One allocation, both strings in it, like the entry's own buffer. */
interp = kmalloc(struct_size(interp, name, nlen + plen + 2),
GFP_KERNEL_ACCOUNT);
@@ -323,7 +331,8 @@ static int entry_attach_interpreter(struct binfmt_misc_entry *e,
strscpy(interp->name, name, nlen + 1);
strscpy(interp->name + nlen + 1, path, plen + 1);
interp->file = f;
- list_add_tail(&interp->list, &e->interps);
+ /* Publish the node: a lockless cat may be walking the list. */
+ list_add_tail_rcu(&interp->list, &e->interps);
pr_debug("register: interpreter: %s {%s}\n", name, path);
return 0;
}
@@ -361,6 +370,20 @@ static void put_binfmt_handler(struct binfmt_misc_entry *e)
DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, put_binfmt_handler(_T))
+/* Drop everything a load program staged for this exec. */
+static void drop_staged_selection(struct linux_binprm *bprm)
+{
+ kfree(bprm->bpf_interp);
+ bprm->bpf_interp = NULL;
+ kfree(bprm->bpf_interp_arg);
+ bprm->bpf_interp_arg = NULL;
+ if (bprm->bpf_interp_file) {
+ fput(bprm->bpf_interp_file);
+ bprm->bpf_interp_file = NULL;
+ }
+ bprm->bpf_flags = 0;
+}
+
/**
* current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace
*
@@ -394,7 +417,8 @@ static struct binfmt_misc *current_binfmt_misc(void)
* @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
+ * handler's load program selects it, either by path or by the name of one
+ * of the interpreters the entry bound. The match is committed, so a failing
* program fails the exec.
*
* Return: the interpreter on success, an ERR_PTR on failure
@@ -404,15 +428,20 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
{
int retval;
+ /*
+ * Drop what a previous chain level staged before anything can pick it
+ * up. A static entry stages nothing but consumes a staged file just
+ * like a 'B' entry does.
+ */
+ drop_staged_selection(bprm);
+
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;
-
+ /* The interpreters this entry lets the program choose from. */
+ bprm->bpf_interps = &e->interps;
retval = e->bpf_ops->load(bprm);
+ bprm->bpf_interps = NULL;
if (retval) {
/* Keep a program-supplied error within errno range. */
if (retval > 0 || retval < -MAX_ERRNO)
@@ -430,9 +459,7 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
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;
+ drop_staged_selection(bprm);
return ERR_PTR(retval);
}
@@ -477,26 +504,36 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
/**
* entry_open_interpreter - open the entry's interpreter for execution
* @e: matched binary type handler
+ * @bprm: binary that is being executed
* @interpreter: the interpreter selected for this exec
*
* An 'F' entry hands out a clone of the file it pre-opened at registration,
- * any other entry opens the selected path.
+ * and so does a 'B' entry whose load program selected one of the
+ * interpreters it bound. Any other entry opens the selected path.
*
* Return: the opened interpreter on success, an ERR_PTR on failure
*/
static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,
+ struct linux_binprm *bprm,
const char *interpreter)
{
struct file *interp_file __free(fput) = NULL;
struct binfmt_misc_interp *interp;
+ struct file *bound;
int retval;
- if (!(e->flags & MISC_FMT_OPEN_FILE))
+ if (bprm->bpf_interp_file) {
+ bound = bprm->bpf_interp_file;
+ } else if (e->flags & MISC_FMT_OPEN_FILE) {
+ /* An 'F' entry pre-opened exactly one interpreter. */
+ interp = list_first_entry(&e->interps,
+ struct binfmt_misc_interp, list);
+ bound = interp->file;
+ } else {
return open_exec(interpreter);
+ }
- /* An 'F' entry pre-opened exactly one interpreter. */
- interp = list_first_entry(&e->interps, struct binfmt_misc_interp, list);
- interp_file = file_clone_open(interp->file);
+ interp_file = file_clone_open(bound);
if (IS_ERR(interp_file))
return interp_file;
@@ -606,7 +643,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
* to the real format in the same round.
*/
if (flags & MISC_FMT_LOADER) {
- interp_file = entry_open_interpreter(fmt, interpreter);
+ interp_file = entry_open_interpreter(fmt, bprm, interpreter);
if (IS_ERR(interp_file)) {
retval = PTR_ERR(interp_file);
/* Declining here would run the binary's own PT_INTERP. */
@@ -628,7 +665,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
if (retval < 0)
return retval;
- interp_file = entry_open_interpreter(fmt, interpreter);
+ interp_file = entry_open_interpreter(fmt, bprm, interpreter);
if (IS_ERR(interp_file))
return PTR_ERR(interp_file);
@@ -902,7 +939,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
/*
* A bpf handler decides the invocation flags per exec with
* bpf_binprm_set_flags() rather than fixing them at registration, and
- * 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B'
+ * the interpreters it binds pre-open what 'F' would have, so a 'B'
* entry carries no invocation flags.
*/
if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
@@ -913,7 +950,8 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
* 'D' is a directive for this registration rather than a lasting
* property, so consume it: the entry is created disabled and stays
* out of the search list until '1' is written to its entry file.
- * The first enable publishes it, for good.
+ * Staying out is what leaves it open to being given interpreters;
+ * the first enable publishes it, for good.
*/
if (e->flags & MISC_FMT_DISABLED) {
e->flags &= ~MISC_FMT_DISABLED;
@@ -955,18 +993,17 @@ enum bm_command {
BM_CMD_REMOVE, /* "-1" */
};
+/* Longest of the commands above, "-1\n". */
+#define MAX_COMMAND_LENGTH 3
+
/*
* Parse what userspace wrote to /status or an entry file: '1' enables,
* '0' disables and '-1' removes the entry or all entries.
*/
-static int parse_command(const char __user *buffer, size_t count)
+static int parse_command(const char *s, size_t count)
{
- char s[4];
-
- if (count > 3)
+ if (count > MAX_COMMAND_LENGTH)
return -EINVAL;
- if (copy_from_user(s, buffer, count))
- return -EFAULT;
if (!count)
return BM_CMD_IGNORE;
if (s[count - 1] == '\n')
@@ -980,6 +1017,18 @@ static int parse_command(const char __user *buffer, size_t count)
return -EINVAL;
}
+/* Copy in a command from a file that takes nothing else, and parse it. */
+static int read_command(const char __user *buffer, size_t count)
+{
+ char s[MAX_COMMAND_LENGTH + 1];
+
+ if (count > sizeof(s) - 1)
+ return -EINVAL;
+ if (copy_from_user(s, buffer, count))
+ return -EFAULT;
+ return parse_command(s, count);
+}
+
/* generic stuff */
/* The root directory's inode; its lock serializes configuring an instance. */
@@ -1003,10 +1052,23 @@ static int bm_entry_show(struct seq_file *m, void *unused)
else
seq_puts(m, "disabled\n");
- if (test_bit(MISC_FMT_BPF_BIT, &e->flags))
+ if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
+ struct binfmt_misc_interp *interp;
+
seq_printf(m, "bpf %s\n", e->bpf_ops->name);
- else
+ /*
+ * A staged entry's set can still grow, so every binding is
+ * rcu-published. The open file pins the entry and with it
+ * every node, so rcu is for the tearing, not the lifetime.
+ */
+ rcu_read_lock();
+ list_for_each_entry_rcu(interp, &e->interps, list)
+ seq_printf(m, "bpf-interpreter %s %s\n",
+ interp->name, interp->path);
+ rcu_read_unlock();
+ } else {
seq_printf(m, "interpreter %s\n", e->interpreter);
+ }
/* print the special flags */
seq_puts(m, "flags: ");
@@ -1201,12 +1263,111 @@ static int bm_entry_open(struct inode *inode, struct file *file)
return 0;
}
+/*
+ * Longest '+<name> <path>' a write can spell, and with it the longest
+ * command an entry file takes: the two delimiters and a newline on top of
+ * the two names.
+ */
+#define MAX_BINDING_LENGTH (BINFMT_MISC_INTERP_NAME_MAX + PATH_MAX + 3)
+
+/**
+ * bm_entry_add_interp - bind another interpreter to a staged entry
+ * @e: the entry
+ * @file: the entry file being written to, for its credentials
+ * @buf: the '+<name> <path>' command, parsed in place and owned by the caller
+ * @count: its length
+ *
+ * A 'D' entry is registered outside the search list, which is what leaves
+ * it open to being configured: it cannot be matched, so no exec can be
+ * holding its interpreters and the set can still grow. Its first enable
+ * publishes it and ends that. One interpreter per write, up to
+ * BINFMT_MISC_INTERP_MAX of them, none of which has to fit in a register
+ * string.
+ *
+ * Return: @count on success, a negative errno on failure
+ */
+static ssize_t bm_entry_add_interp(struct binfmt_misc_entry *e,
+ struct file *file, char *buf, size_t count)
+{
+ struct file *f __free(close_interp_file) = NULL;
+ struct inode *root = bm_root_inode(file_inode(file)->i_sb);
+ size_t nlen, plen;
+ char *name, *path;
+ int retval;
+
+ /* Settled before the open: type is fixed, publication is permanent. */
+ if (!test_bit(MISC_FMT_BPF_BIT, &e->flags))
+ return -EINVAL;
+ if (!hlist_unhashed_lockless(&e->node))
+ return -EBUSY;
+
+ /* '+<name> <path>': the path is everything past the first space. */
+ name = buf + 1;
+ path = strchr(name, ' ');
+ if (!path)
+ return -EINVAL;
+ *path++ = '\0';
+
+ plen = strlen(path);
+ /* The command has to end at the write, like a register string. */
+ if (path + plen != buf + count)
+ return -EINVAL;
+ if (plen && path[plen - 1] == '\n')
+ path[--plen] = '\0';
+ /* Resolved now, so a relative path would name the writer's cwd. */
+ if (path[0] != '/')
+ return -EINVAL;
+
+ nlen = path - name - 1;
+ if (!nlen || nlen > BINFMT_MISC_INTERP_NAME_MAX)
+ return -EINVAL;
+ /* The name prints between delimiters, so keep it a printable word. */
+ for (const char *p = name; *p; p++)
+ if (!isascii(*p) || !isgraph(*p))
+ return -EINVAL;
+
+ /* Opened before the lock: resolving it may walk this very filesystem. */
+ f = open_interp_file(file->f_cred, path);
+ if (IS_ERR(f))
+ return PTR_ERR(f);
+
+ inode_lock(root);
+ if (d_unhashed(e->dentry))
+ retval = -ENOENT; /* removed while we were opening it */
+ else if (!hlist_unhashed(&e->node))
+ retval = -EBUSY; /* published while we were opening it */
+ else
+ retval = entry_attach_interpreter(e, name, path, f);
+ inode_unlock(root);
+ if (retval)
+ return retval;
+
+ /* The file is owned by the entry now. */
+ retain_and_null_ptr(f);
+ return count;
+}
+
static ssize_t bm_entry_write(struct file *file, const char __user *buffer,
size_t count, loff_t *ppos)
{
struct inode *inode = file_inode(file);
struct binfmt_misc_entry *e = inode->i_private;
- int res = parse_command(buffer, count);
+ char *buf __free(kfree) = NULL;
+ int res;
+
+ /* A binding is the longest command this file takes. */
+ if (count > MAX_BINDING_LENGTH)
+ return -E2BIG;
+
+ buf = memdup_user_nul(buffer, count);
+ if (IS_ERR(buf))
+ return PTR_ERR(buf);
+
+ /* '+<name> <path>' binds an interpreter, everything else toggles. */
+ if (buf[0] == '+')
+ return bm_entry_add_interp(e, file, buf, count);
+
+ res = parse_command(buf, count);
switch (res) {
case BM_CMD_DISABLE:
@@ -1218,8 +1379,9 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer,
/*
* The first enable publishes a 'D' entry into the search
* list, whole. The lock keeps that ordered against a second
- * enable and against removal; a removed entry has nothing
- * left to publish.
+ * enable, against removal - a removed entry has nothing left
+ * to publish - and against binding: what can be matched can
+ * no longer be configured.
*/
inode_lock(root);
set_bit(MISC_FMT_ENABLED_BIT, &e->flags);
@@ -1348,7 +1510,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer,
size_t count, loff_t *ppos)
{
struct binfmt_misc *misc;
- int res = parse_command(buffer, count);
+ int res = read_command(buffer, count);
misc = i_binfmt_misc(file_inode(file));
switch (res) {
diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c
index 92003ea640d3..b246d886437b 100644
--- a/fs/binfmt_misc_bpf.c
+++ b/fs/binfmt_misc_bpf.c
@@ -7,6 +7,15 @@
* namespace it was registered in. A binfmt_misc 'B' entry activates it:
*
* echo ':entry:B::::<handler-name>:' > <binfmt_misc>/register
+ *
+ * The entry can bind the interpreters the handler may run its binaries
+ * with, each opened by the write that binds it and selected by name per
+ * exec. An entry registered with 'D' is not matchable yet, which is what
+ * leaves it open to being given them:
+ *
+ * echo ':entry:B::::<handler-name>:D' > <binfmt_misc>/register
+ * echo '+<name> <path>' > <binfmt_misc>/entry
+ * echo 1 > <binfmt_misc>/entry
*/
#include <linux/binfmt_misc.h>
@@ -16,6 +25,8 @@
#include <linux/btf.h>
#include <linux/btf_ids.h>
#include <linux/cred.h>
+#include <linux/file.h>
+#include <linux/fs.h>
#include <linux/init.h>
#include <linux/limits.h>
#include <linux/slab.h>
@@ -91,6 +102,20 @@ bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog)
prog->aux->st_ops == &bpf_binfmt_misc_ops;
}
+/*
+ * Replace the staged interpreter selection: naming a path drops a bound
+ * file, selecting a bound interpreter carries its file along.
+ */
+static void bm_bpf_stage_selection(struct linux_binprm *bprm, char *path,
+ struct file *f)
+{
+ if (bprm->bpf_interp_file)
+ fput(bprm->bpf_interp_file);
+ kfree(bprm->bpf_interp);
+ bprm->bpf_interp = path;
+ bprm->bpf_interp_file = f;
+}
+
__bpf_kfunc_start_defs();
/**
@@ -103,7 +128,8 @@ __bpf_kfunc_start_defs();
* 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.
+ * returns. Calling it again replaces the selection, as does selecting an
+ * interpreter the entry bound with bpf_binprm_select_interp().
*
* Return: 0 on success, a negative errno on failure
*/
@@ -127,8 +153,50 @@ __bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm,
if (!interp)
return -ENOMEM;
- kfree(bprm->bpf_interp);
- bprm->bpf_interp = interp;
+ bm_bpf_stage_selection(bprm, interp, NULL);
+ return 0;
+}
+
+/**
+ * bpf_binprm_select_interp - run this exec under an interpreter the entry bound
+ * @bprm: binary that is being executed
+ * @name: name the interpreter was registered under
+ * @name__sz: size of the @name buffer, including the terminating NUL
+ *
+ * To be called from the load program of a struct binfmt_misc_ops handler
+ * instead of bpf_binprm_set_interp(). It selects one of the interpreters
+ * the matched entry was registered with, each of which was opened once when
+ * the entry was registered. Nothing is resolved at exec time, so no
+ * filesystem view can redirect the interpreter.
+ *
+ * The interpreter runs under the path the entry registered it under.
+ * Calling it again replaces the selection.
+ *
+ * Return: 0 on success, -ENOENT if the matched entry bound no interpreter
+ * of that name, a negative errno on failure
+ */
+__bpf_kfunc int bpf_binprm_select_interp(struct linux_binprm *bprm,
+ const char *name, size_t name__sz)
+{
+ const struct binfmt_misc_interp *interp;
+ size_t len;
+ char *path;
+
+ if (!name__sz)
+ return -EINVAL;
+ len = strnlen(name, name__sz);
+ if (len == name__sz || !len)
+ return -EINVAL;
+
+ interp = binfmt_misc_find_interp(bprm->bpf_interps, name);
+ if (!interp)
+ return -ENOENT;
+
+ path = kstrdup(interp->path, GFP_KERNEL);
+ if (!path)
+ return -ENOMEM;
+
+ bm_bpf_stage_selection(bprm, path, get_file(interp->file));
return 0;
}
@@ -211,6 +279,7 @@ __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_select_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)
diff --git a/fs/exec.c b/fs/exec.c
index 856731f78d05..a14f28b15607 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1477,6 +1477,8 @@ static void free_bprm(struct linux_binprm *bprm)
if (bprm->interp != bprm->filename)
kfree(bprm->interp);
kfree(bprm->bpf_interp);
+ if (bprm->bpf_interp_file)
+ fput(bprm->bpf_interp_file);
kfree(bprm->bpf_interp_arg);
kfree(bprm->fdpath);
kfree(bprm);
diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h
index 4abdfd36b3fa..072e4b3dd78d 100644
--- a/include/linux/binfmt_misc.h
+++ b/include/linux/binfmt_misc.h
@@ -5,11 +5,41 @@
#include <linux/types.h>
struct bpf_prog;
+struct file;
struct linux_binprm;
struct user_namespace;
#define BINFMT_MISC_OPS_NAME_MAX 16
+/* Longest name a 'B' entry can bind an interpreter under. */
+#define BINFMT_MISC_INTERP_NAME_MAX 32
+
+/* Most interpreters one entry can bind. */
+#define BINFMT_MISC_INTERP_MAX 100
+
+/**
+ * struct binfmt_misc_interp - an interpreter an entry was registered with
+ * @list: link in the entry's list, in registration order
+ * @file: the file, opened at registration and never resolved again
+ * @path: the path it was registered under, used as the name the interpreter
+ * runs under; stored after @name in the same allocation
+ * @name: the name the load program selects it by; empty for the fixed
+ * interpreter of a static 'F' entry
+ *
+ * Owned by the entry and living exactly as long as it does. The list head
+ * is handed to the handler's load program for the duration of one exec,
+ * which picks one with bpf_binprm_select_interp().
+ */
+struct binfmt_misc_interp {
+ struct list_head list;
+ struct file *file;
+ const char *path;
+ char name[];
+};
+
+const struct binfmt_misc_interp *
+binfmt_misc_find_interp(const struct list_head *interps, const char *name);
+
/**
* 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)
@@ -42,10 +72,11 @@ enum bpf_binprm_flags {
* 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
+ * bpf_binprm_set_interp(), or one the entry bound via
+ * bpf_binprm_select_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 {
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index a2daecbb01d6..f686a37f7a0a 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -14,7 +14,10 @@ struct coredump_params;
/* Interpreter selection staged by a bpf binfmt_misc handler. */
struct binfmt_misc_bpf {
+ /* interpreters the matched entry bound, selectable by name */
+ const struct list_head *bpf_interps;
const char *bpf_interp; /* interpreter selected by a bpf handler */
+ struct file *bpf_interp_file; /* the bound interpreter it selected */
const char *bpf_interp_arg; /* interpreter argument from a bpf handler */
u64 bpf_flags; /* enum bpf_binprm_flags from a bpf handler */
};
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 8/9] selftests/exec: test interpreters bound to a 'B' entry
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
` (6 preceding siblings ...)
2026-07-30 13:34 ` [PATCH 7/9] binfmt_misc: let a 'B' entry bind its interpreters Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
2026-07-30 13:34 ` [PATCH 9/9] binfmt_misc: document interpreters bound by " Christian Brauner
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
One handler, one entry registered disabled, an interpreter per guest
architecture bound to a file one write at a time. The load program picks
one by name per exec:
- an aarch64 binary runs the interpreter bound as "first" and a riscv one
the interpreter bound as "second", from a single entry and a single
handler
- unlinking a bound interpreter and putting a different binary in its
place changes nothing, which is what the binding exists for
- the entry reports what it bound, under the names it bound them as
- a name the entry did not bind fails the exec with -ENOENT rather than
falling back to anything
- activating the entry refuses further binding with -EBUSY, a later
disable does not undo that, and an entry registered without 'D' never
accepted a '+' write to begin with
- a name binds one interpreter, and control characters are refused
- the command has to end at the write, bytes past an embedded nul are
refused
- an entry binds at most 100 interpreters, the next one is refused with
-ENOSPC
The test interpreter prints its argv[0], which is the path the kernel ran
that copy under, so one binary installed at two paths tells the harness
which of them the program picked.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/exec/Makefile | 7 +-
tools/testing/selftests/exec/binfmt_bind_interp.c | 14 ++
tools/testing/selftests/exec/binfmt_misc_bpf.c | 261 +++++++++++++++++++++-
tools/testing/selftests/exec/interp_bind.bpf.c | 76 +++++++
4 files changed, 349 insertions(+), 9 deletions(-)
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index ec7894a802e0..410c93606a0c 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -56,8 +56,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
ifeq ($(HAVE_BPF_TOOLCHAIN),y)
TEST_GEN_PROGS += binfmt_misc_bpf
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
-TEST_GEN_FILES += loader.bpf.o
-TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
+TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o
+TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp
else
$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
endif
@@ -127,6 +127,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
diff --git a/tools/testing/selftests/exec/binfmt_bind_interp.c b/tools/testing/selftests/exec/binfmt_bind_interp.c
new file mode 100644
index 000000000000..06d65062856b
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bind_interp.c
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test interpreter for the bound-interpreter case of the binfmt_misc_bpf
+ * selftest. Two copies are installed at different paths and bound to one
+ * entry under different names; printing argv[0] - the path the kernel ran
+ * this copy under - tells the harness which of them the load program picked.
+ */
+#include <stdio.h>
+
+int main(int argc, char **argv)
+{
+ printf("BIND_RAN %s\n", argc > 0 ? argv[0] : "");
+ return 0;
+}
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index 71bb6d8b4517..2c7b63075f1d 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -9,7 +9,7 @@
*
* echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
*
- * Three self-contained cases are exercised:
+ * Five 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
@@ -26,6 +26,11 @@
* (binfmt_loader_payload) runs as the main image with the selected
* interpreter substituted for its PT_INTERP and asserts the native
* identity from inside.
+ * 5. interp_bind: an entry registered disabled with 'D' is given its
+ * interpreters one write at a time, and the load program picks one by
+ * name per exec. Replacing what the path holds afterwards changes
+ * nothing, which is the point of binding a file rather than resolving
+ * a name at exec time. Enabling the entry seals it.
*
* The first two route to a test interpreter that prints BPF_INTERP_RAN,
* proving the program's chosen interpreter actually ran.
@@ -54,6 +59,12 @@
#define TRANS_EXPECT "TRANSPARENT_OK"
#define LOADER_INTERP "/tmp/binfmt_loader_interp"
#define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest"
+#define BIND_FIRST "/tmp/binfmt_bind_first"
+#define BIND_SECOND "/tmp/binfmt_bind_second"
+#define BIND_ARM_PATH "/tmp/binfmt_bind_arm"
+#define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv"
+#define BIND_EXPECT "BIND_RAN "
+#define BIND_MAX 100
/* A minimal 64-bit little-endian ELF header, padded to the read size. */
static int create_fake_elf(const char *path, unsigned short machine)
@@ -82,11 +93,17 @@ static int create_fake_elf(const char *path, unsigned short machine)
return 0;
}
-static int register_entry(const char *name, const char *handler)
+/*
+ * Register a 'B' entry for @handler. With @flags "D" the entry is created
+ * disabled, which is what leaves it open to being given interpreters.
+ */
+static int register_entry(const char *name, const char *handler,
+ const char *flags)
{
char rule[PATH_MAX];
- snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler);
+ snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler,
+ flags ? flags : "");
return write_reg(rule);
}
@@ -139,10 +156,12 @@ struct bpf_case {
/*
* Load @objfile, attach its struct_ops map @handler (which publishes the
- * handler) and activate a 'B' entry named @entry that references it.
+ * handler) and register a 'B' entry named @entry that references it, with
+ * @flags as the entry's register-string flags.
*/
-static int bpf_case_start(struct bpf_case *c, const char *objfile,
- const char *handler, const char *entry)
+static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,
+ const char *handler, const char *entry,
+ const char *flags)
{
struct bpf_map *map;
@@ -172,7 +191,7 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
c->link = NULL;
goto fail;
}
- if (register_entry(entry, handler)) {
+ if (register_entry(entry, handler, flags)) {
fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
goto fail;
}
@@ -186,6 +205,12 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
return -1;
}
+static int bpf_case_start(struct bpf_case *c, const char *objfile,
+ const char *handler, const char *entry)
+{
+ return bpf_case_start_flags(c, objfile, handler, entry, NULL);
+}
+
static void bpf_case_stop(struct bpf_case *c)
{
unregister(c->entry);
@@ -318,4 +343,226 @@ TEST_F(bpf_handler, loader_substitution)
unlink(LOADER_INTERP);
}
+/* The errno an exec of @path fails with, 0 if it succeeded. */
+static int exec_errno(const char *path)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid == 0) {
+ execl(path, path, (char *)NULL);
+ _exit(errno);
+ }
+ if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+/* Install a copy of the bound-interpreter test binary at @path. */
+static int install_interp(const char *path)
+{
+ char src[PATH_MAX];
+
+ if (artifact_path(src, sizeof(src), "binfmt_bind_interp"))
+ return -1;
+ return copy_file(src, path);
+}
+
+/* Bind @path to @entry under @name, the '+' command of a disabled entry. */
+static int entry_bind(const char *entry, const char *name, const char *path)
+{
+ char cmd[PATH_MAX];
+
+ snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
+ return entry_command(entry, cmd);
+}
+
+FIXTURE(bound_interp) {
+ char obj[PATH_MAX];
+ struct bpf_case c;
+ bool started;
+};
+
+FIXTURE_SETUP(bound_interp)
+{
+ const char *why = bpf_handler_unsupported();
+
+ if (why)
+ SKIP(return, "%s", why);
+ if (!binfmt_flag_supported('D')) {
+ ASSERT_EQ(errno, EINVAL);
+ SKIP(return, "kernel without the 'D' flag");
+ }
+
+ ASSERT_EQ(install_interp(BIND_FIRST), 0);
+ ASSERT_EQ(install_interp(BIND_SECOND), 0);
+
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "interp_bind.bpf.o"), 0);
+
+ /*
+ * Registered disabled, so it cannot be matched yet and can still be
+ * given interpreters. Each path is resolved once, by its write(2);
+ * from here on the entry holds the files themselves.
+ */
+ ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind",
+ "test_interp_bind", "D"), 0);
+ self->started = true;
+
+ ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0);
+ ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0);
+}
+
+FIXTURE_TEARDOWN(bound_interp)
+{
+ if (self->started)
+ bpf_case_stop(&self->c);
+ unlink(BIND_FIRST);
+ unlink(BIND_SECOND);
+ unlink(AARCH64_PATH);
+ unlink(BIND_RISCV_PATH);
+ unlink(BIND_ARM_PATH);
+}
+
+/* Enabling is what makes the configured entry matchable. */
+static int activate(const char *entry)
+{
+ return entry_command(entry, "1\n");
+}
+
+/* One entry, one interpreter per guest architecture, picked per exec. */
+TEST_F(bound_interp, selects_by_name)
+{
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);
+
+ /* Disabled, so it does not match and no format claims the binary. */
+ EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);
+
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+ EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+ EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);
+}
+
+/* What was bound is what runs, whatever the path holds afterwards. */
+TEST_F(bound_interp, path_no_longer_decides)
+{
+ char other[PATH_MAX];
+
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ /* Bound interpreters are pinned against writes, exactly like 'F'. */
+ EXPECT_TRUE(write_denied(BIND_FIRST));
+
+ /* Replace the path with a different binary: a new file, new inode. */
+ ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0);
+ ASSERT_EQ(unlink(BIND_FIRST), 0);
+ ASSERT_EQ(copy_file(other, BIND_FIRST), 0);
+
+ EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+}
+
+/* The entry reports what it bound, under the names it bound them as. */
+TEST_F(bound_interp, entry_reports_bindings)
+{
+ EXPECT_TRUE(entry_shows("test_interp_bind",
+ "bpf-interpreter first " BIND_FIRST));
+ EXPECT_TRUE(entry_shows("test_interp_bind",
+ "bpf-interpreter second " BIND_SECOND));
+}
+
+/* Selecting a name the entry did not bind fails the exec. */
+TEST_F(bound_interp, unbound_name_fails)
+{
+ ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);
+}
+
+/* Activating seals it: what can be matched cannot be changed. */
+TEST_F(bound_interp, sealed_once_active)
+{
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+ EXPECT_FALSE(entry_shows("test_interp_bind",
+ "bpf-interpreter third " BIND_SECOND));
+}
+
+/* The seal is for good: disabling the entry again reopens nothing. */
+TEST_F(bound_interp, disable_does_not_unseal)
+{
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+ ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0);
+
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+}
+
+/* An entry registered without 'D' is sealed from the start. */
+TEST_F(bound_interp, born_sealed)
+{
+ /* A second entry for the handler the fixture already published. */
+ ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0);
+
+ EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY);
+ unregister("test_born_sealed");
+}
+
+/* A name is bound once; a second use of it is refused. */
+TEST_F(bound_interp, duplicate_name_refused)
+{
+ EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST);
+}
+
+/* A name is a printable word: the entry file reports 'name path' lines. */
+TEST_F(bound_interp, name_must_be_printable)
+{
+ /* A control character would forge a line into the entry file. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL);
+ EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL);
+
+ /* A space cannot even be spelled: the path starts after the first one. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL);
+}
+
+/* The command ends at the write: bytes past an embedded nul are refused. */
+TEST_F(bound_interp, trailing_bytes_refused)
+{
+ char cmd[PATH_MAX];
+ size_t len;
+ int fd;
+
+ /* entry_command() cannot spell a nul, so write the buffer raw. */
+ snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST);
+ len = strlen(cmd) + 1;
+ memcpy(cmd + len, "junk", sizeof("junk"));
+ len += sizeof("junk");
+
+ fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(write(fd, cmd, len), -1);
+ EXPECT_EQ(errno, EINVAL);
+ close(fd);
+
+ EXPECT_FALSE(entry_shows("test_interp_bind",
+ "bpf-interpreter nul " BIND_FIRST));
+}
+
+/* An entry binds at most BIND_MAX interpreters. */
+TEST_F(bound_interp, capped_bindings)
+{
+ char name[16];
+ int i;
+
+ /* The fixture bound "first" and "second" already. */
+ for (i = 2; i < BIND_MAX; i++) {
+ snprintf(name, sizeof(name), "n%d", i);
+ ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0);
+ }
+ EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/interp_bind.bpf.c b/tools/testing/selftests/exec/interp_bind.bpf.c
new file mode 100644
index 000000000000..1ce45cca215f
--- /dev/null
+++ b/tools/testing/selftests/exec/interp_bind.bpf.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binfmt_misc_ops handler for the selftest's bound-interpreter case: one
+ * handler, one entry, an interpreter per guest architecture - each bound to
+ * a file when the entry was registered rather than to a path resolved at
+ * exec time. The load program names the one it wants; a name the entry did
+ * not bind fails the exec, which the harness checks too.
+ */
+#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 E_MACHINE_OFF 18
+#define EM_ARM 40
+#define EM_AARCH64 183
+#define EM_RISCV 243
+
+extern int bpf_binprm_select_interp(struct linux_binprm *bprm,
+ const char *name, size_t name__sz) __ksym;
+
+/* The guest architecture of a 64-bit ELF, or zero if it is not one. */
+static __u16 elf_machine(struct linux_binprm *bprm)
+{
+ if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
+ bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
+ bprm->buf[EI_CLASS] != ELFCLASS64)
+ return 0;
+
+ /* Little-endian 16-bit field, read byte-wise for the verifier. */
+ return (__u8)bprm->buf[E_MACHINE_OFF] |
+ ((__u16)(__u8)bprm->buf[E_MACHINE_OFF + 1] << 8);
+}
+
+SEC("struct_ops.s/match")
+bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm)
+{
+ __u16 machine = elf_machine(bprm);
+
+ return machine == EM_AARCH64 || machine == EM_RISCV ||
+ machine == EM_ARM;
+}
+
+SEC("struct_ops.s/load")
+int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)
+{
+ /*
+ * Names, not paths: each one selects a file the entry pre-opened, so
+ * nothing is resolved here or later, in any namespace. The buffers
+ * are on the stack because the verifier rejects .rodata for a sized
+ * memory argument.
+ */
+ char first[] = "first";
+ char second[] = "second";
+ char unbound[] = "unbound";
+
+ switch (elf_machine(bprm)) {
+ case EM_AARCH64:
+ return bpf_binprm_select_interp(bprm, first, sizeof(first));
+ case EM_RISCV:
+ return bpf_binprm_select_interp(bprm, second, sizeof(second));
+ }
+
+ /* The entry bound nothing under this name: -ENOENT fails the exec. */
+ return bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));
+}
+
+SEC(".struct_ops.link")
+struct binfmt_misc_ops interp_bind = {
+ .match = (void *)interp_bind_match,
+ .load = (void *)interp_bind_load,
+ .name = "interp_bind",
+};
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH 9/9] binfmt_misc: document interpreters bound by a 'B' entry
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
` (7 preceding siblings ...)
2026-07-30 13:34 ` [PATCH 8/9] selftests/exec: test interpreters bound to a 'B' entry Christian Brauner
@ 2026-07-30 13:34 ` Christian Brauner
8 siblings, 0 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-30 13:34 UTC (permalink / raw)
To: linux-fsdevel
Cc: Alexander Viro, Jan Kara, Kees Cook, linux-mm, bpf,
Jonathan Corbet, Farid Zakaria, Daniel Borkmann,
Alexei Starovoitov, jannh, mail, Christian Brauner (Amutable)
Describe the interpreters a 'B' entry can bind while it is disabled, what
binding a file buys over naming a path the exec resolves, how a load
program picks one, that an entry binds at most 100 interpreters, and
that enabling the entry seals the set.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Documentation/admin-guide/binfmt-misc.rst | 66 ++++++++++++++++++++++++++++---
1 file changed, 60 insertions(+), 6 deletions(-)
diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 8254ddcb3389..3e2b2a9415bc 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -195,9 +195,62 @@ 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.
+entries; ``-ENOEXEC`` lets the remaining binary formats have a go. A path
+selected this way is opened with the credentials of the task doing the
+exec, exactly as a statically registered interpreter without ``F`` would
+be.
+
+An entry can instead bind the interpreters its handler may use, so that no
+path is resolved at exec time at all. An entry registered with ``D`` is not
+matchable yet, which is what leaves it open to being given them, one
+``+name path`` write at a time::
+
+ echo ':qemu:B::::my_handler:D' > register
+ echo '+aarch64 /usr/bin/qemu-aarch64' > qemu
+ echo '+arm /usr/bin/qemu-arm' > qemu
+ echo 1 > qemu
+
+Each path is opened during its write, in the writing process's context and
+with the credentials the entry file was opened with, exactly the way ``F``
+pre-opens a static entry's interpreter; the paths must be absolute. The
+path is everything past the first space, so there is nothing it cannot
+express, and no interpreter has to fit in a register string. An entry
+binds at most 100 interpreters; a write past that is refused with
+``-ENOSPC``. To bind a file that has no path of its own - already
+unlinked, a ``memfd``, or reachable only in another mount namespace -
+open it and write ``/proc/self/fd/N``.
+
+The ``load`` program then selects one per exec by name with the
+``bpf_binprm_select_interp()`` kfunc, and every exec runs a clone of the
+file that was opened. The path decides which file is bound and nothing
+else: it is not resolved again, in any namespace, so what it holds later -
+or what it holds in the namespace of whoever runs the binary - no longer
+decides anything.
+
+Enabling the entry ends this. Its interpreters are read at exec time with
+nothing but a reference held on the entry, so an entry that has ever been
+matchable can never have its set changed again: the first ``1`` seals it,
+from then on ``+`` is refused with ``-EBUSY``, and an entry registered
+without ``D`` is sealed from the start. Binding a name twice is refused
+with ``-EEXIST``.
+
+Selection is by name so that the configuration and the program need not
+agree on an order, and so that a handler is not tied to where a distribution
+puts its interpreters. A name is a single word of printable ASCII, at most
+32 characters; a name the entry did not bind gives the program ``-ENOENT``,
+which it can act on or return. The interpreter runs under the path it was
+registered under, and the entry reports what it bound::
+
+ $ cat /proc/sys/fs/binfmt_misc/qemu
+ enabled
+ bpf my_handler
+ bpf-interpreter aarch64 /usr/bin/qemu-aarch64
+ bpf-interpreter arm /usr/bin/qemu-arm
+ flags:
+
+The path reported is the one the interpreter was bound under, which named
+the file at that moment; it is not re-resolved, so it is a record of what
+was bound rather than a promise about what that path holds now.
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
@@ -234,9 +287,10 @@ handler can decide them differently for each binary it handles:
flag). It excludes the other flags and a staged interpreter argument.
Because these are program choices, a ``B`` entry carries no invocation
-flags in the register string; ``F`` (pre-open a fixed interpreter) has no
-meaning for it. The registration directive ``D`` is the exception: it
-decides how the entry starts out, not how the interpreter is invoked.
+flags in the register string; ``F`` has none to spell for it either, since
+the interpreters it binds already pre-open what ``F`` would. The
+registration directive ``D`` is the exception: it decides how the entry
+starts out, not how the interpreter is invoked.
Handlers are looked up in the user namespace the struct_ops map was
registered in, falling back to ancestor namespaces, mirroring how
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-07-30 13:35 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
2026-07-30 13:34 ` [PATCH 1/9] binfmt_misc: let a register string create an entry disabled Christian Brauner
2026-07-30 13:34 ` [PATCH 2/9] selftests/exec: let binfmt_flag_supported() return a bool Christian Brauner
2026-07-30 13:34 ` [PATCH 3/9] selftests/exec: test registering an entry disabled Christian Brauner
2026-07-30 13:34 ` [PATCH 4/9] binfmt_misc: document " Christian Brauner
2026-07-30 13:34 ` [PATCH 5/9] selftests/exec: share the bpf handler preconditions Christian Brauner
2026-07-30 13:34 ` [PATCH 6/9] binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp Christian Brauner
2026-07-30 13:34 ` [PATCH 7/9] binfmt_misc: let a 'B' entry bind its interpreters Christian Brauner
2026-07-30 13:34 ` [PATCH 8/9] selftests/exec: test interpreters bound to a 'B' entry Christian Brauner
2026-07-30 13:34 ` [PATCH 9/9] binfmt_misc: document interpreters bound by " Christian Brauner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox