AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/01 02:56 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "BINFMT_MISC",
    "BINFMT_MISC_BPF"
  ],
  "FocusSymbols": [
    "binfmt_misc_find_interp",
    "bpf_binprm_select_interp",
    "bpf_binprm_set_interp",
    "load_misc_binary",
    "bm_entry_write",
    "bm_register_write"
  ],
  "Reasoning": "The patch introduces a new feature to binfmt_misc allowing BPF handlers to bind interpreters to an entry and select them by name at exec time. This modifies core kernel logic in fs/binfmt_misc.c and fs/binfmt_misc_bpf.c and is reachable from userspace via binfmt_misc configuration and execution of matching binaries.",
  "WorthFuzzing": true
}

1/1 2026/08/01 02:56 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit df9251a60da166224bea9baa3bd9c42f74b9d5ca\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sat Aug 1 02:56:01 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst\nindex c80702b4ccd5f..3e2b2a9415bc8 100644\n--- a/Documentation/admin-guide/binfmt-misc.rst\n+++ b/Documentation/admin-guide/binfmt-misc.rst\n@@ -107,6 +107,15 @@ Here is what the fields mean:\n             ``PT_INTERP``. See the \"Loader substitution\" section\n             below. ``L`` rejects ``T``, ``P``, ``O`` and ``C``;\n             ``F`` composes.\n+      ``D`` - registered disabled\n+            The entry is created disabled instead of being matchable at\n+            once, and has to be enabled by writing ``1`` to its file\n+            before it dispatches anything. This splits a registration\n+            into creating the entry and activating it, leaving room to\n+            configure it in between - which is what a ``B`` entry that\n+            binds interpreters needs; see the bpf section below. The flag\n+            is spent on the registration and is not read back: what an\n+            entry file reports afterwards is whether it is enabled.\n \n \n There are some restrictions:\n@@ -186,9 +195,62 @@ and derive the interpreter from the binary's location. It selects the\n interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an\n absolute path and returning ``0``. A match is committed: a failing\n ``load`` fails the exec with its error instead of falling through to later\n-entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The\n-interpreter is opened with the credentials of the task doing the exec,\n-exactly as a statically registered interpreter would be.\n+entries; ``-ENOEXEC`` lets the remaining binary formats have a go. A path\n+selected this way is opened with the credentials of the task doing the\n+exec, exactly as a statically registered interpreter without ``F`` would\n+be.\n+\n+An entry can instead bind the interpreters its handler may use, so that no\n+path is resolved at exec time at all. An entry registered with ``D`` is not\n+matchable yet, which is what leaves it open to being given them, one\n+``+name path`` write at a time::\n+\n+    echo ':qemu:B::::my_handler:D' \u003e register\n+    echo '+aarch64 /usr/bin/qemu-aarch64' \u003e qemu\n+    echo '+arm /usr/bin/qemu-arm' \u003e qemu\n+    echo 1 \u003e qemu\n+\n+Each path is opened during its write, in the writing process's context and\n+with the credentials the entry file was opened with, exactly the way ``F``\n+pre-opens a static entry's interpreter; the paths must be absolute. The\n+path is everything past the first space, so there is nothing it cannot\n+express, and no interpreter has to fit in a register string. An entry\n+binds at most 100 interpreters; a write past that is refused with\n+``-ENOSPC``. To bind a file that has no path of its own - already\n+unlinked, a ``memfd``, or reachable only in another mount namespace -\n+open it and write ``/proc/self/fd/N``.\n+\n+The ``load`` program then selects one per exec by name with the\n+``bpf_binprm_select_interp()`` kfunc, and every exec runs a clone of the\n+file that was opened. The path decides which file is bound and nothing\n+else: it is not resolved again, in any namespace, so what it holds later -\n+or what it holds in the namespace of whoever runs the binary - no longer\n+decides anything.\n+\n+Enabling the entry ends this. Its interpreters are read at exec time with\n+nothing but a reference held on the entry, so an entry that has ever been\n+matchable can never have its set changed again: the first ``1`` seals it,\n+from then on ``+`` is refused with ``-EBUSY``, and an entry registered\n+without ``D`` is sealed from the start. Binding a name twice is refused\n+with ``-EEXIST``.\n+\n+Selection is by name so that the configuration and the program need not\n+agree on an order, and so that a handler is not tied to where a distribution\n+puts its interpreters. A name is a single word of printable ASCII, at most\n+32 characters; a name the entry did not bind gives the program ``-ENOENT``,\n+which it can act on or return. The interpreter runs under the path it was\n+registered under, and the entry reports what it bound::\n+\n+    $ cat /proc/sys/fs/binfmt_misc/qemu\n+    enabled\n+    bpf my_handler\n+    bpf-interpreter aarch64 /usr/bin/qemu-aarch64\n+    bpf-interpreter arm /usr/bin/qemu-arm\n+    flags:\n+\n+The path reported is the one the interpreter was bound under, which named\n+the file at that moment; it is not re-resolved, so it is a record of what\n+was bound rather than a promise about what that path holds now.\n \n The ``load`` program can also pass a single argument to the interpreter with\n the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the\n@@ -224,8 +286,11 @@ handler can decide them differently for each binary it handles:\n   ``PT_INTERP`` and runs the binary as a fully native exec (the ``L``\n   flag). It excludes the other flags and a staged interpreter argument.\n \n-Because these are program choices, a ``B`` entry carries no flags in the\n-register string; ``F`` (pre-open a fixed interpreter) has no meaning for it.\n+Because these are program choices, a ``B`` entry carries no invocation\n+flags in the register string; ``F`` has none to spell for it either, since\n+the interpreters it binds already pre-open what ``F`` would. The\n+registration directive ``D`` is the exception: it decides how the entry\n+starts out, not how the interpreter is invoked.\n \n Handlers are looked up in the user namespace the struct_ops map was\n registered in, falling back to ancestor namespaces, mirroring how\ndiff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c\nindex 707f8a14f8a63..ad8c4f64bf102 100644\n--- a/fs/binfmt_misc.c\n+++ b/fs/binfmt_misc.c\n@@ -24,6 +24,8 @@\n #include \u003clinux/fs_context.h\u003e\n #include \u003clinux/init.h\u003e\n #include \u003clinux/kstrtox.h\u003e\n+#include \u003clinux/limits.h\u003e\n+#include \u003clinux/list.h\u003e\n #include \u003clinux/magic.h\u003e\n #include \u003clinux/module.h\u003e\n #include \u003clinux/printk.h\u003e\n@@ -37,6 +39,8 @@\n #include \u003clinux/uaccess.h\u003e\n #include \u003clinux/user_namespace.h\u003e\n \n+#include \"internal.h\"\n+\n /* Entry status and match type bit numbers. */\n enum binfmt_misc_entry_bits {\n \tMISC_FMT_ENABLED_BIT\t= 0,\n@@ -52,8 +56,17 @@ enum binfmt_misc_entry_flags {\n \tMISC_FMT_OPEN_FILE\t= (1U \u003c\u003c 28),\n \tMISC_FMT_TRANSPARENT\t= (1U \u003c\u003c 27),\n \tMISC_FMT_LOADER\t\t= (1U \u003c\u003c 26),\n+\tMISC_FMT_DISABLED\t= (1U \u003c\u003c 25),\n };\n \n+/* The flags that shape the invocation; a 'B' handler picks those per exec. */\n+#define MISC_FMT_INVOCATION_FLAGS (MISC_FMT_PRESERVE_ARGV0 |\t\\\n+\t\t\t\t   MISC_FMT_OPEN_BINARY |\t\\\n+\t\t\t\t   MISC_FMT_CREDENTIALS |\t\\\n+\t\t\t\t   MISC_FMT_OPEN_FILE |\t\t\\\n+\t\t\t\t   MISC_FMT_TRANSPARENT |\t\\\n+\t\t\t\t   MISC_FMT_LOADER)\n+\n /**\n  * struct binfmt_misc_flag - a flag character of the register string\n  * @c: the character userspace writes and reads back\n@@ -75,6 +88,7 @@ static const struct binfmt_misc_flag misc_flags[] = {\n \t{ 'F', MISC_FMT_OPEN_FILE,\t0,\t\t\t\"open interpreter file now\"\t},\n \t{ 'T', MISC_FMT_TRANSPARENT,\tMISC_FMT_OPEN_BINARY,\t\"transparent\"\t\t\t},\n \t{ 'L', MISC_FMT_LOADER,\t\t0,\t\t\t\"loader substitution\"\t\t},\n+\t{ 'D', MISC_FMT_DISABLED,\t0,\t\t\t\"register disabled\"\t\t},\n };\n \n /* Look up a flag character, NULL if @c is not one. */\n@@ -96,9 +110,9 @@ struct binfmt_misc_entry {\n \tconst char *interpreter;\t/* filename of interpreter */\n \tchar *name;\n \tstruct dentry *dentry;\n-\tstruct file *interp_file;\n \tconst struct binfmt_misc_ops *bpf_ops;\t/* bpf-backed handler ('B') */\n \tconst char *bpf_ops_name;\n+\tstruct list_head interps;\t/* the interpreters it bound */\n \trefcount_t users;\t\t/* sync removal with load_misc_binary() */\n \tstruct rcu_head rcu;\n \tchar buf[];\t\t\t/* register string, fields point in here */\n@@ -175,7 +189,12 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm)\n \t/* Walk all the registered handlers. */\n \thlist_for_each_entry_rcu(e, \u0026misc-\u003eentries, node,\n \t\t\t\t srcu_read_lock_held(\u0026bm_entries_srcu)) {\n-\t\t/* Make sure this one is currently enabled. */\n+\t\t/*\n+\t\t * Make sure this one is currently enabled. An entry enters\n+\t\t * the list at most once and only whole: its configuration is\n+\t\t * ordered before the rcu insertion that makes it visible\n+\t\t * here.\n+\t\t */\n \t\tif (!test_bit(MISC_FMT_ENABLED_BIT, \u0026e-\u003eflags))\n \t\t\tcontinue;\n \n@@ -216,6 +235,108 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc,\n \treturn search_binfmt_handler(misc, bprm);\n }\n \n+/**\n+ * binfmt_misc_find_interp - find a bound interpreter by name\n+ * @interps: the interpreters the matched entry was registered with\n+ * @name: the name to look for\n+ *\n+ * Return: the interpreter on success, NULL if @interps has none by that name\n+ */\n+const struct binfmt_misc_interp *\n+binfmt_misc_find_interp(const struct list_head *interps, const char *name)\n+{\n+\tstruct binfmt_misc_interp *interp;\n+\n+\tlist_for_each_entry(interp, interps, list)\n+\t\tif (!strcmp(interp-\u003ename, name))\n+\t\t\treturn interp;\n+\treturn NULL;\n+}\n+\n+/* Undo the open_exec() a pre-opened interpreter file came from. */\n+static void close_interp_file(struct file *f)\n+{\n+\tif (IS_ERR_OR_NULL(f))\n+\t\treturn;\n+\texe_file_allow_write_access(f);\n+\tfilp_close(f, NULL);\n+}\n+\n+DEFINE_FREE(close_interp_file, struct file *, close_interp_file(_T))\n+\n+/*\n+ * Open an interpreter @path for execution: now, in the writer's context,\n+ * and - since binfmt_misc mounts can be unprivileged - with @cred, the\n+ * credentials the control file being written was opened with, not the\n+ * writer's own.\n+ */\n+static struct file *open_interp_file(const struct cred *cred, const char *path)\n+{\n+\tstruct file *f;\n+\n+\tscoped_with_creds(cred)\n+\t\tf = open_exec(path);\n+\tif (IS_ERR(f))\n+\t\tpr_notice(\"register: failed to install interpreter %s\\n\", path);\n+\treturn f;\n+}\n+\n+/* Release the interpreters an entry was registered with. */\n+static void entry_put_interpreters(struct binfmt_misc_entry *e)\n+{\n+\tstruct binfmt_misc_interp *interp, *tmp;\n+\n+\tlist_for_each_entry_safe(interp, tmp, \u0026e-\u003einterps, list) {\n+\t\tlist_del(\u0026interp-\u003elist);\n+\t\tclose_interp_file(interp-\u003efile);\n+\t\tkfree(interp);\n+\t}\n+}\n+\n+/**\n+ * entry_attach_interpreter - bind an opened interpreter to @e\n+ * @e: entry being configured\n+ * @name: name the load program will select it by; empty for the fixed\n+ *        interpreter of a static entry\n+ * @path: the path @f was opened from\n+ * @f: the interpreter, opened for execution\n+ *\n+ * Every exec runs a clone of @f, so the path decided which file is bound\n+ * and nothing else: it is not resolved again, in any namespace.\n+ *\n+ * The caller has to have validated @name and @path, established that @e\n+ * cannot be matched yet, and owns @f until this succeeds.\n+ *\n+ * Return: 0 on success, a negative errno on failure\n+ */\n+static int entry_attach_interpreter(struct binfmt_misc_entry *e,\n+\t\t\t\t    const char *name, const char *path,\n+\t\t\t\t    struct file *f)\n+{\n+\tsize_t nlen = strlen(name), plen = strlen(path);\n+\tstruct binfmt_misc_interp *interp;\n+\n+\tif (binfmt_misc_find_interp(\u0026e-\u003einterps, name))\n+\t\treturn -EEXIST;\n+\tif (list_count_nodes(\u0026e-\u003einterps) \u003e= BINFMT_MISC_INTERP_MAX)\n+\t\treturn -ENOSPC;\n+\n+\t/* One allocation, both strings in it, like the entry's own buffer. */\n+\tinterp = kmalloc(struct_size(interp, name, nlen + plen + 2),\n+\t\t\t GFP_KERNEL_ACCOUNT);\n+\tif (!interp)\n+\t\treturn -ENOMEM;\n+\n+\tinterp-\u003epath = interp-\u003ename + nlen + 1;\n+\tstrscpy(interp-\u003ename, name, nlen + 1);\n+\tstrscpy(interp-\u003ename + nlen + 1, path, plen + 1);\n+\tinterp-\u003efile = f;\n+\t/* Publish the node: a lockless cat may be walking the list. */\n+\tlist_add_tail_rcu(\u0026interp-\u003elist, \u0026e-\u003einterps);\n+\tpr_debug(\"register: interpreter: %s {%s}\\n\", name, path);\n+\treturn 0;\n+}\n+\n static void bm_entry_free_rcu(struct rcu_head *rcu)\n {\n \tstruct binfmt_misc_entry *e = container_of(rcu, struct binfmt_misc_entry, rcu);\n@@ -232,21 +353,36 @@ static void bm_entry_free_rcu(struct rcu_head *rcu)\n  *\n  * Free entry syncing with load_misc_binary() and defer final free to\n  * load_misc_binary() in case it is using the binary type handler we were\n- * requested to remove.\n+ * requested to remove. Also the teardown for a registration that fails\n+ * before add_entry() publishes the entry.\n  */\n static void put_binfmt_handler(struct binfmt_misc_entry *e)\n {\n+\tif (IS_ERR_OR_NULL(e))\n+\t\treturn;\n+\n \tif (refcount_dec_and_test(\u0026e-\u003eusers)) {\n-\t\tif (e-\u003eflags \u0026 MISC_FMT_OPEN_FILE) {\n-\t\t\texe_file_allow_write_access(e-\u003einterp_file);\n-\t\t\tfilp_close(e-\u003einterp_file, NULL);\n-\t\t}\n+\t\tentry_put_interpreters(e);\n \t\t/* Walkers may still dereference this entry, even sleeping. */\n \t\tcall_srcu(\u0026bm_entries_srcu, \u0026e-\u003ercu, bm_entry_free_rcu);\n \t}\n }\n \n-DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, if (_T) put_binfmt_handler(_T))\n+DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, put_binfmt_handler(_T))\n+\n+/* Drop everything a load program staged for this exec. */\n+static void drop_staged_selection(struct linux_binprm *bprm)\n+{\n+\tkfree(bprm-\u003ebpf_interp);\n+\tbprm-\u003ebpf_interp = NULL;\n+\tkfree(bprm-\u003ebpf_interp_arg);\n+\tbprm-\u003ebpf_interp_arg = NULL;\n+\tif (bprm-\u003ebpf_interp_file) {\n+\t\tfput(bprm-\u003ebpf_interp_file);\n+\t\tbprm-\u003ebpf_interp_file = NULL;\n+\t}\n+\tbprm-\u003ebpf_flags = 0;\n+}\n \n /**\n  * current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace\n@@ -281,7 +417,8 @@ static struct binfmt_misc *current_binfmt_misc(void)\n  * @bprm: binary that is being executed\n  *\n  * A static entry carries its interpreter path, for a 'B' entry the\n- * handler's load program selects it. The match is committed, so a failing\n+ * handler's load program selects it, either by path or by the name of one\n+ * of the interpreters the entry bound. The match is committed, so a failing\n  * program fails the exec.\n  *\n  * Return: the interpreter on success, an ERR_PTR on failure\n@@ -291,15 +428,20 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,\n {\n \tint retval;\n \n+\t/*\n+\t * Drop what a previous chain level staged before anything can pick it\n+\t * up. A static entry stages nothing but consumes a staged file just\n+\t * like a 'B' entry does.\n+\t */\n+\tdrop_staged_selection(bprm);\n+\n \tif (!test_bit(MISC_FMT_BPF_BIT, \u0026e-\u003eflags))\n \t\treturn e-\u003einterpreter;\n \n-\t/* Drop any interpreter or flags a previous chain level staged. */\n-\tkfree(bprm-\u003ebpf_interp);\n-\tbprm-\u003ebpf_interp = NULL;\n-\tbprm-\u003ebpf_flags = 0;\n-\n+\t/* The interpreters this entry lets the program choose from. */\n+\tbprm-\u003ebpf_interps = \u0026e-\u003einterps;\n \tretval = e-\u003ebpf_ops-\u003eload(bprm);\n+\tbprm-\u003ebpf_interps = NULL;\n \tif (retval) {\n \t\t/* Keep a program-supplied error within errno range. */\n \t\tif (retval \u003e 0 || retval \u003c -MAX_ERRNO)\n@@ -317,9 +459,7 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,\n \n drop_staged:\n \t/* A failing load leaves nothing behind for later entries. */\n-\tkfree(bprm-\u003ebpf_interp_arg);\n-\tbprm-\u003ebpf_interp_arg = NULL;\n-\tbprm-\u003ebpf_flags = 0;\n+\tdrop_staged_selection(bprm);\n \treturn ERR_PTR(retval);\n }\n \n@@ -364,23 +504,36 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,\n /**\n  * entry_open_interpreter - open the entry's interpreter for execution\n  * @e: matched binary type handler\n+ * @bprm: binary that is being executed\n  * @interpreter: the interpreter selected for this exec\n  *\n  * An 'F' entry hands out a clone of the file it pre-opened at registration,\n- * any other entry opens the selected path.\n+ * and so does a 'B' entry whose load program selected one of the\n+ * interpreters it bound. Any other entry opens the selected path.\n  *\n  * Return: the opened interpreter on success, an ERR_PTR on failure\n  */\n static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,\n+\t\t\t\t\t   struct linux_binprm *bprm,\n \t\t\t\t\t   const char *interpreter)\n {\n \tstruct file *interp_file __free(fput) = NULL;\n+\tstruct binfmt_misc_interp *interp;\n+\tstruct file *bound;\n \tint retval;\n \n-\tif (!(e-\u003eflags \u0026 MISC_FMT_OPEN_FILE))\n+\tif (bprm-\u003ebpf_interp_file) {\n+\t\tbound = bprm-\u003ebpf_interp_file;\n+\t} else if (e-\u003eflags \u0026 MISC_FMT_OPEN_FILE) {\n+\t\t/* An 'F' entry pre-opened exactly one interpreter. */\n+\t\tinterp = list_first_entry(\u0026e-\u003einterps,\n+\t\t\t\t\t  struct binfmt_misc_interp, list);\n+\t\tbound = interp-\u003efile;\n+\t} else {\n \t\treturn open_exec(interpreter);\n+\t}\n \n-\tinterp_file = file_clone_open(e-\u003einterp_file);\n+\tinterp_file = file_clone_open(bound);\n \tif (IS_ERR(interp_file))\n \t\treturn interp_file;\n \n@@ -490,7 +643,7 @@ static int load_misc_binary(struct linux_binprm *bprm)\n \t * to the real format in the same round.\n \t */\n \tif (flags \u0026 MISC_FMT_LOADER) {\n-\t\tinterp_file = entry_open_interpreter(fmt, interpreter);\n+\t\tinterp_file = entry_open_interpreter(fmt, bprm, interpreter);\n \t\tif (IS_ERR(interp_file)) {\n \t\t\tretval = PTR_ERR(interp_file);\n \t\t\t/* Declining here would run the binary's own PT_INTERP. */\n@@ -512,7 +665,7 @@ static int load_misc_binary(struct linux_binprm *bprm)\n \tif (retval \u003c 0)\n \t\treturn retval;\n \n-\tinterp_file = entry_open_interpreter(fmt, interpreter);\n+\tinterp_file = entry_open_interpreter(fmt, bprm, interpreter);\n \tif (IS_ERR(interp_file))\n \t\treturn PTR_ERR(interp_file);\n \n@@ -684,7 +837,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,\n \t\t\t\t\t      size_t count)\n {\n \tstruct binfmt_misc_entry *e __free(kfree) = NULL;\n-\tchar *buf, *p, *flags;\n+\tchar *buf, *p;\n \tchar del;\n \n \tpr_debug(\"register: received %zu bytes\\n\", count);\n@@ -701,6 +854,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,\n \tp = buf = e-\u003ebuf;\n \n \tmemset(e, 0, sizeof(*e));\n+\tINIT_LIST_HEAD(\u0026e-\u003einterps);\n \tif (copy_from_user(buf, buffer, count))\n \t\treturn ERR_PTR(-EFAULT);\n \n@@ -780,18 +934,30 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,\n \t}\n \n \t/* Parse the 'flags' field. */\n-\tflags = p;\n \tp = check_special_flags(p, e);\n \n \t/*\n \t * A bpf handler decides the invocation flags per exec with\n \t * bpf_binprm_set_flags() rather than fixing them at registration, and\n-\t * 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B'\n-\t * entry's flags field has to be empty.\n+\t * the interpreters it binds pre-open what 'F' would have, so a 'B'\n+\t * entry carries no invocation flags.\n \t */\n-\tif (test_bit(MISC_FMT_BPF_BIT, \u0026e-\u003eflags) \u0026\u0026 p != flags)\n+\tif (test_bit(MISC_FMT_BPF_BIT, \u0026e-\u003eflags) \u0026\u0026\n+\t    (e-\u003eflags \u0026 MISC_FMT_INVOCATION_FLAGS))\n \t\treturn ERR_PTR(-EINVAL);\n \n+\t/*\n+\t * 'D' is a directive for this registration rather than a lasting\n+\t * property, so consume it: the entry is created disabled and stays\n+\t * out of the search list until '1' is written to its entry file.\n+\t * Staying out is what leaves it open to being given interpreters;\n+\t * the first enable publishes it, for good.\n+\t */\n+\tif (e-\u003eflags \u0026 MISC_FMT_DISABLED) {\n+\t\te-\u003eflags \u0026= ~MISC_FMT_DISABLED;\n+\t\tclear_bit(MISC_FMT_ENABLED_BIT, \u0026e-\u003eflags);\n+\t}\n+\n \t/* Transparency preserves the whole argv, argv[0] included. */\n \tif ((e-\u003eflags \u0026 MISC_FMT_TRANSPARENT) \u0026\u0026\n \t    (e-\u003eflags \u0026 MISC_FMT_PRESERVE_ARGV0))\n@@ -814,6 +980,8 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,\n \t    e-\u003einterpreter[0] != '/')\n \t\treturn ERR_PTR(-EINVAL);\n \n+\t/* Born holding one reference; put_binfmt_handler() is the teardown. */\n+\trefcount_set(\u0026e-\u003eusers, 1);\n \treturn no_free_ptr(e);\n }\n \n@@ -825,18 +993,17 @@ enum bm_command {\n \tBM_CMD_REMOVE,\t/* \"-1\" */\n };\n \n+/* Longest of the commands above, \"-1\\n\". */\n+#define MAX_COMMAND_LENGTH 3\n+\n /*\n  * Parse what userspace wrote to /status or an entry file: '1' enables,\n  * '0' disables and '-1' removes the entry or all entries.\n  */\n-static int parse_command(const char __user *buffer, size_t count)\n+static int parse_command(const char *s, size_t count)\n {\n-\tchar s[4];\n-\n-\tif (count \u003e 3)\n+\tif (count \u003e MAX_COMMAND_LENGTH)\n \t\treturn -EINVAL;\n-\tif (copy_from_user(s, buffer, count))\n-\t\treturn -EFAULT;\n \tif (!count)\n \t\treturn BM_CMD_IGNORE;\n \tif (s[count - 1] == '\\n')\n@@ -850,8 +1017,26 @@ static int parse_command(const char __user *buffer, size_t count)\n \treturn -EINVAL;\n }\n \n+/* Copy in a command from a file that takes nothing else, and parse it. */\n+static int read_command(const char __user *buffer, size_t count)\n+{\n+\tchar s[MAX_COMMAND_LENGTH + 1];\n+\n+\tif (count \u003e sizeof(s) - 1)\n+\t\treturn -EINVAL;\n+\tif (copy_from_user(s, buffer, count))\n+\t\treturn -EFAULT;\n+\treturn parse_command(s, count);\n+}\n+\n /* generic stuff */\n \n+/* The root directory's inode; its lock serializes configuring an instance. */\n+static struct inode *bm_root_inode(struct super_block *sb)\n+{\n+\treturn d_inode(sb-\u003es_root);\n+}\n+\n static void bm_seq_hex(struct seq_file *m, const u8 *data, int size)\n {\n \tfor (int i = 0; i \u003c size; i++)\n@@ -867,10 +1052,23 @@ static int bm_entry_show(struct seq_file *m, void *unused)\n \telse\n \t\tseq_puts(m, \"disabled\\n\");\n \n-\tif (test_bit(MISC_FMT_BPF_BIT, \u0026e-\u003eflags))\n+\tif (test_bit(MISC_FMT_BPF_BIT, \u0026e-\u003eflags)) {\n+\t\tstruct binfmt_misc_interp *interp;\n+\n \t\tseq_printf(m, \"bpf %s\\n\", e-\u003ebpf_ops-\u003ename);\n-\telse\n+\t\t/*\n+\t\t * A staged entry's set can still grow, so every binding is\n+\t\t * rcu-published. The open file pins the entry and with it\n+\t\t * every node, so rcu is for the tearing, not the lifetime.\n+\t\t */\n+\t\trcu_read_lock();\n+\t\tlist_for_each_entry_rcu(interp, \u0026e-\u003einterps, list)\n+\t\t\tseq_printf(m, \"bpf-interpreter %s %s\\n\",\n+\t\t\t\t   interp-\u003ename, interp-\u003epath);\n+\t\trcu_read_unlock();\n+\t} else {\n \t\tseq_printf(m, \"interpreter %s\\n\", e-\u003einterpreter);\n+\t}\n \n \t/* print the special flags */\n \tseq_puts(m, \"flags: \");\n@@ -992,10 +1190,11 @@ static void remove_binfmt_handler(struct binfmt_misc *misc,\n /* Remove @e unless it was already removed. */\n static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb)\n {\n-\tstruct inode *root = d_inode(sb-\u003es_root);\n+\tstruct inode *root = bm_root_inode(sb);\n \n \tinode_lock_nested(root, I_MUTEX_PARENT);\n-\tif (!hlist_unhashed(\u0026e-\u003enode))\n+\t/* A staged entry is not hashed; the dentry says if it was removed. */\n+\tif (!d_unhashed(e-\u003edentry))\n \t\tremove_binfmt_handler(i_binfmt_misc(root), e);\n \tinode_unlock(root);\n }\n@@ -1004,13 +1203,21 @@ static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb)\n static void bm_remove_all_entries(struct binfmt_misc *misc,\n \t\t\t\t  struct super_block *sb)\n {\n-\tstruct inode *root = d_inode(sb-\u003es_root);\n-\tstruct binfmt_misc_entry *e;\n-\tstruct hlist_node *next;\n+\tstruct inode *root = bm_root_inode(sb);\n+\tstruct dentry *child = NULL;\n \n \tinode_lock_nested(root, I_MUTEX_PARENT);\n-\thlist_for_each_entry_safe(e, next, \u0026misc-\u003eentries, node)\n-\t\tremove_binfmt_handler(misc, e);\n+\t/*\n+\t * Walk the directory rather than the search list: a staged entry\n+\t * is in the former but not yet in the latter. The control files\n+\t * carry no entry and stay.\n+\t */\n+\twhile ((child = find_next_child(sb-\u003es_root, child))) {\n+\t\tstruct binfmt_misc_entry *e = d_inode(child)-\u003ei_private;\n+\n+\t\tif (e)\n+\t\t\tremove_binfmt_handler(misc, e);\n+\t}\n \tinode_unlock(root);\n }\n \n@@ -1056,20 +1263,138 @@ static int bm_entry_open(struct inode *inode, struct file *file)\n \treturn 0;\n }\n \n+/*\n+ * Longest '+\u003cname\u003e \u003cpath\u003e' a write can spell, and with it the longest\n+ * command an entry file takes: the two delimiters and a newline on top of\n+ * the two names.\n+ */\n+#define MAX_BINDING_LENGTH (BINFMT_MISC_INTERP_NAME_MAX + PATH_MAX + 3)\n+\n+/**\n+ * bm_entry_add_interp - bind another interpreter to a staged entry\n+ * @e: the entry\n+ * @file: the entry file being written to, for its credentials\n+ * @buf: the '+\u003cname\u003e \u003cpath\u003e' command, parsed in place and owned by the caller\n+ * @count: its length\n+ *\n+ * A 'D' entry is registered outside the search list, which is what leaves\n+ * it open to being configured: it cannot be matched, so no exec can be\n+ * holding its interpreters and the set can still grow. Its first enable\n+ * publishes it and ends that. One interpreter per write, up to\n+ * BINFMT_MISC_INTERP_MAX of them, none of which has to fit in a register\n+ * string.\n+ *\n+ * Return: @count on success, a negative errno on failure\n+ */\n+static ssize_t bm_entry_add_interp(struct binfmt_misc_entry *e,\n+\t\t\t\t   struct file *file, char *buf, size_t count)\n+{\n+\tstruct file *f __free(close_interp_file) = NULL;\n+\tstruct inode *root = bm_root_inode(file_inode(file)-\u003ei_sb);\n+\tsize_t nlen, plen;\n+\tchar *name, *path;\n+\tint retval;\n+\n+\t/* Settled before the open: type is fixed, publication is permanent. */\n+\tif (!test_bit(MISC_FMT_BPF_BIT, \u0026e-\u003eflags))\n+\t\treturn -EINVAL;\n+\tif (!hlist_unhashed_lockless(\u0026e-\u003enode))\n+\t\treturn -EBUSY;\n+\n+\t/* '+\u003cname\u003e \u003cpath\u003e': the path is everything past the first space. */\n+\tname = buf + 1;\n+\tpath = strchr(name, ' ');\n+\tif (!path)\n+\t\treturn -EINVAL;\n+\t*path++ = '\\0';\n+\n+\tplen = strlen(path);\n+\t/* The command has to end at the write, like a register string. */\n+\tif (path + plen != buf + count)\n+\t\treturn -EINVAL;\n+\tif (plen \u0026\u0026 path[plen - 1] == '\\n')\n+\t\tpath[--plen] = '\\0';\n+\t/* Resolved now, so a relative path would name the writer's cwd. */\n+\tif (path[0] != '/')\n+\t\treturn -EINVAL;\n+\n+\tnlen = path - name - 1;\n+\tif (!nlen || nlen \u003e BINFMT_MISC_INTERP_NAME_MAX)\n+\t\treturn -EINVAL;\n+\t/* The name prints between delimiters, so keep it a printable word. */\n+\tfor (const char *p = name; *p; p++)\n+\t\tif (!isascii(*p) || !isgraph(*p))\n+\t\t\treturn -EINVAL;\n+\n+\t/* Opened before the lock: resolving it may walk this very filesystem. */\n+\tf = open_interp_file(file-\u003ef_cred, path);\n+\tif (IS_ERR(f))\n+\t\treturn PTR_ERR(f);\n+\n+\tinode_lock(root);\n+\tif (d_unhashed(e-\u003edentry))\n+\t\tretval = -ENOENT;\t/* removed while we were opening it */\n+\telse if (!hlist_unhashed(\u0026e-\u003enode))\n+\t\tretval = -EBUSY;\t/* published while we were opening it */\n+\telse\n+\t\tretval = entry_attach_interpreter(e, name, path, f);\n+\tinode_unlock(root);\n+\tif (retval)\n+\t\treturn retval;\n+\n+\t/* The file is owned by the entry now. */\n+\tretain_and_null_ptr(f);\n+\treturn count;\n+}\n+\n static ssize_t bm_entry_write(struct file *file, const char __user *buffer,\n \t\t\t\tsize_t count, loff_t *ppos)\n {\n \tstruct inode *inode = file_inode(file);\n \tstruct binfmt_misc_entry *e = inode-\u003ei_private;\n-\tint res = parse_command(buffer, count);\n+\tchar *buf __free(kfree) = NULL;\n+\tint res;\n+\n+\t/* A binding is the longest command this file takes. */\n+\tif (count \u003e MAX_BINDING_LENGTH)\n+\t\treturn -E2BIG;\n+\n+\tbuf = memdup_user_nul(buffer, count);\n+\tif (IS_ERR(buf))\n+\t\treturn PTR_ERR(buf);\n+\n+\t/* '+\u003cname\u003e \u003cpath\u003e' binds an interpreter, everything else toggles. */\n+\tif (buf[0] == '+')\n+\t\treturn bm_entry_add_interp(e, file, buf, count);\n+\n+\tres = parse_command(buf, count);\n \n \tswitch (res) {\n \tcase BM_CMD_DISABLE:\n \t\tclear_bit(MISC_FMT_ENABLED_BIT, \u0026e-\u003eflags);\n \t\tbreak;\n-\tcase BM_CMD_ENABLE:\n+\tcase BM_CMD_ENABLE: {\n+\t\tstruct inode *root = bm_root_inode(inode-\u003ei_sb);\n+\n+\t\t/*\n+\t\t * The first enable publishes a 'D' entry into the search\n+\t\t * list, whole. The lock keeps that ordered against a second\n+\t\t * enable, against removal - a removed entry has nothing left\n+\t\t * to publish - and against binding: what can be matched can\n+\t\t * no longer be configured.\n+\t\t */\n+\t\tinode_lock(root);\n \t\tset_bit(MISC_FMT_ENABLED_BIT, \u0026e-\u003eflags);\n+\t\tif (hlist_unhashed(\u0026e-\u003enode) \u0026\u0026 !d_unhashed(e-\u003edentry)) {\n+\t\t\tstruct binfmt_misc *misc = i_binfmt_misc(inode);\n+\n+\t\t\tspin_lock(\u0026misc-\u003eentries_lock);\n+\t\t\thlist_add_head_rcu(\u0026e-\u003enode, \u0026misc-\u003eentries);\n+\t\t\tspin_unlock(\u0026misc-\u003eentries_lock);\n+\t\t}\n+\t\tinode_unlock(root);\n \t\tbreak;\n+\t}\n \tcase BM_CMD_REMOVE:\n \t\tbm_remove_entry(e, inode-\u003ei_sb);\n \t\tbreak;\n@@ -1106,16 +1431,18 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb)\n \t\treturn -ENOMEM;\n \t}\n \n-\trefcount_set(\u0026e-\u003eusers, 1);\n \te-\u003edentry = dentry;\n \tinode-\u003ei_private = e;\n \tinode-\u003ei_fop = \u0026bm_entry_operations;\n \n \td_make_persistent(dentry, inode);\n-\tmisc = i_binfmt_misc(inode);\n-\tspin_lock(\u0026misc-\u003eentries_lock);\n-\thlist_add_head_rcu(\u0026e-\u003enode, \u0026misc-\u003eentries);\n-\tspin_unlock(\u0026misc-\u003eentries_lock);\n+\t/* A 'D' entry stays out of the search list until its first enable. */\n+\tif (test_bit(MISC_FMT_ENABLED_BIT, \u0026e-\u003eflags)) {\n+\t\tmisc = i_binfmt_misc(inode);\n+\t\tspin_lock(\u0026misc-\u003eentries_lock);\n+\t\thlist_add_head_rcu(\u0026e-\u003enode, \u0026misc-\u003eentries);\n+\t\tspin_unlock(\u0026misc-\u003eentries_lock);\n+\t}\n \tsimple_done_creating(dentry);\n \treturn 0;\n }\n@@ -1123,9 +1450,8 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb)\n static ssize_t bm_register_write(struct file *file, const char __user *buffer,\n \t\t\t       size_t count, loff_t *ppos)\n {\n-\tstruct binfmt_misc_entry *e __free(kfree) = NULL;\n+\tstruct binfmt_misc_entry *e __free(put_binfmt_handler) = NULL;\n \tstruct super_block *sb = file_inode(file)-\u003ei_sb;\n-\tstruct file *f = NULL;\n \tint err;\n \n \te = create_entry(buffer, count);\n@@ -1142,33 +1468,20 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer,\n \t}\n \n \tif (e-\u003eflags \u0026 MISC_FMT_OPEN_FILE) {\n-\t\t/*\n-\t\t * Now that we support unprivileged binfmt_misc mounts make\n-\t\t * sure we use the credentials that the register @file was\n-\t\t * opened with to also open the interpreter. Before that this\n-\t\t * didn't matter much as only a privileged process could open\n-\t\t * the register file.\n-\t\t */\n-\t\tscoped_with_creds(file-\u003ef_cred)\n-\t\t\tf = open_exec(e-\u003einterpreter);\n-\t\tif (IS_ERR(f)) {\n-\t\t\tpr_notice(\"register: failed to install interpreter file %s\\n\",\n-\t\t\t\t e-\u003einterpreter);\n+\t\tstruct file *f = open_interp_file(file-\u003ef_cred, e-\u003einterpreter);\n+\n+\t\tif (IS_ERR(f))\n \t\t\treturn PTR_ERR(f);\n+\t\terr = entry_attach_interpreter(e, \"\", e-\u003einterpreter, f);\n+\t\tif (err) {\n+\t\t\tclose_interp_file(f);\n+\t\t\treturn err;\n \t\t}\n-\t\te-\u003einterp_file = f;\n \t}\n \n \terr = add_entry(e, sb);\n-\tif (err) {\n-\t\tif (f) {\n-\t\t\texe_file_allow_write_access(f);\n-\t\t\tfilp_close(f, NULL);\n-\t\t}\n-\t\tif (e-\u003ebpf_ops)\n-\t\t\tbinfmt_misc_put_ops(e-\u003ebpf_ops);\n+\tif (err)\n \t\treturn err;\n-\t}\n \n \t/* The entry is owned by its inode now. */\n \tretain_and_null_ptr(e);\n@@ -1197,7 +1510,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer,\n \t\tsize_t count, loff_t *ppos)\n {\n \tstruct binfmt_misc *misc;\n-\tint res = parse_command(buffer, count);\n+\tint res = read_command(buffer, count);\n \n \tmisc = i_binfmt_misc(file_inode(file));\n \tswitch (res) {\ndiff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c\nindex 92003ea640d3f..b246d886437b8 100644\n--- a/fs/binfmt_misc_bpf.c\n+++ b/fs/binfmt_misc_bpf.c\n@@ -7,6 +7,15 @@\n  * namespace it was registered in. A binfmt_misc 'B' entry activates it:\n  *\n  *   echo ':entry:B::::\u003chandler-name\u003e:' \u003e \u003cbinfmt_misc\u003e/register\n+ *\n+ * The entry can bind the interpreters the handler may run its binaries\n+ * with, each opened by the write that binds it and selected by name per\n+ * exec. An entry registered with 'D' is not matchable yet, which is what\n+ * leaves it open to being given them:\n+ *\n+ *   echo ':entry:B::::\u003chandler-name\u003e:D' \u003e \u003cbinfmt_misc\u003e/register\n+ *   echo '+\u003cname\u003e \u003cpath\u003e' \u003e \u003cbinfmt_misc\u003e/entry\n+ *   echo 1 \u003e \u003cbinfmt_misc\u003e/entry\n  */\n \n #include \u003clinux/binfmt_misc.h\u003e\n@@ -16,6 +25,8 @@\n #include \u003clinux/btf.h\u003e\n #include \u003clinux/btf_ids.h\u003e\n #include \u003clinux/cred.h\u003e\n+#include \u003clinux/file.h\u003e\n+#include \u003clinux/fs.h\u003e\n #include \u003clinux/init.h\u003e\n #include \u003clinux/limits.h\u003e\n #include \u003clinux/slab.h\u003e\n@@ -91,6 +102,20 @@ bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog)\n \t       prog-\u003eaux-\u003est_ops == \u0026bpf_binfmt_misc_ops;\n }\n \n+/*\n+ * Replace the staged interpreter selection: naming a path drops a bound\n+ * file, selecting a bound interpreter carries its file along.\n+ */\n+static void bm_bpf_stage_selection(struct linux_binprm *bprm, char *path,\n+\t\t\t\t   struct file *f)\n+{\n+\tif (bprm-\u003ebpf_interp_file)\n+\t\tfput(bprm-\u003ebpf_interp_file);\n+\tkfree(bprm-\u003ebpf_interp);\n+\tbprm-\u003ebpf_interp = path;\n+\tbprm-\u003ebpf_interp_file = f;\n+}\n+\n __bpf_kfunc_start_defs();\n \n /**\n@@ -103,7 +128,8 @@ __bpf_kfunc_start_defs();\n  * before returning zero; the verifier rejects the call from any other\n  * program, including the handler's own match program. The path is opened\n  * with the credentials of the task doing the exec after the program\n- * returns.\n+ * returns. Calling it again replaces the selection, as does selecting an\n+ * interpreter the entry bound with bpf_binprm_select_interp().\n  *\n  * Return: 0 on success, a negative errno on failure\n  */\n@@ -127,8 +153,50 @@ __bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm,\n \tif (!interp)\n \t\treturn -ENOMEM;\n \n-\tkfree(bprm-\u003ebpf_interp);\n-\tbprm-\u003ebpf_interp = interp;\n+\tbm_bpf_stage_selection(bprm, interp, NULL);\n+\treturn 0;\n+}\n+\n+/**\n+ * bpf_binprm_select_interp - run this exec under an interpreter the entry bound\n+ * @bprm: binary that is being executed\n+ * @name: name the interpreter was registered under\n+ * @name__sz: size of the @name buffer, including the terminating NUL\n+ *\n+ * To be called from the load program of a struct binfmt_misc_ops handler\n+ * instead of bpf_binprm_set_interp(). It selects one of the interpreters\n+ * the matched entry was registered with, each of which was opened once when\n+ * the entry was registered. Nothing is resolved at exec time, so no\n+ * filesystem view can redirect the interpreter.\n+ *\n+ * The interpreter runs under the path the entry registered it under.\n+ * Calling it again replaces the selection.\n+ *\n+ * Return: 0 on success, -ENOENT if the matched entry bound no interpreter\n+ * of that name, a negative errno on failure\n+ */\n+__bpf_kfunc int bpf_binprm_select_interp(struct linux_binprm *bprm,\n+\t\t\t\t\t const char *name, size_t name__sz)\n+{\n+\tconst struct binfmt_misc_interp *interp;\n+\tsize_t len;\n+\tchar *path;\n+\n+\tif (!name__sz)\n+\t\treturn -EINVAL;\n+\tlen = strnlen(name, name__sz);\n+\tif (len == name__sz || !len)\n+\t\treturn -EINVAL;\n+\n+\tinterp = binfmt_misc_find_interp(bprm-\u003ebpf_interps, name);\n+\tif (!interp)\n+\t\treturn -ENOENT;\n+\n+\tpath = kstrdup(interp-\u003epath, GFP_KERNEL);\n+\tif (!path)\n+\t\treturn -ENOMEM;\n+\n+\tbm_bpf_stage_selection(bprm, path, get_file(interp-\u003efile));\n \treturn 0;\n }\n \n@@ -211,6 +279,7 @@ __bpf_kfunc_end_defs();\n \n BTF_KFUNCS_START(bm_bpf_kfunc_ids)\n BTF_ID_FLAGS(func, bpf_binprm_set_interp, KF_SLEEPABLE)\n+BTF_ID_FLAGS(func, bpf_binprm_select_interp, KF_SLEEPABLE)\n BTF_ID_FLAGS(func, bpf_binprm_set_interp_arg, KF_SLEEPABLE)\n BTF_ID_FLAGS(func, bpf_binprm_set_flags, KF_SLEEPABLE)\n BTF_KFUNCS_END(bm_bpf_kfunc_ids)\ndiff --git a/fs/exec.c b/fs/exec.c\nindex 856731f78d054..a14f28b15607a 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1477,6 +1477,8 @@ static void free_bprm(struct linux_binprm *bprm)\n \tif (bprm-\u003einterp != bprm-\u003efilename)\n \t\tkfree(bprm-\u003einterp);\n \tkfree(bprm-\u003ebpf_interp);\n+\tif (bprm-\u003ebpf_interp_file)\n+\t\tfput(bprm-\u003ebpf_interp_file);\n \tkfree(bprm-\u003ebpf_interp_arg);\n \tkfree(bprm-\u003efdpath);\n \tkfree(bprm);\ndiff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h\nindex 4abdfd36b3fa8..072e4b3dd78d2 100644\n--- a/include/linux/binfmt_misc.h\n+++ b/include/linux/binfmt_misc.h\n@@ -5,11 +5,41 @@\n #include \u003clinux/types.h\u003e\n \n struct bpf_prog;\n+struct file;\n struct linux_binprm;\n struct user_namespace;\n \n #define BINFMT_MISC_OPS_NAME_MAX 16\n \n+/* Longest name a 'B' entry can bind an interpreter under. */\n+#define BINFMT_MISC_INTERP_NAME_MAX 32\n+\n+/* Most interpreters one entry can bind. */\n+#define BINFMT_MISC_INTERP_MAX 100\n+\n+/**\n+ * struct binfmt_misc_interp - an interpreter an entry was registered with\n+ * @list: link in the entry's list, in registration order\n+ * @file: the file, opened at registration and never resolved again\n+ * @path: the path it was registered under, used as the name the interpreter\n+ *        runs under; stored after @name in the same allocation\n+ * @name: the name the load program selects it by; empty for the fixed\n+ *        interpreter of a static 'F' entry\n+ *\n+ * Owned by the entry and living exactly as long as it does. The list head\n+ * is handed to the handler's load program for the duration of one exec,\n+ * which picks one with bpf_binprm_select_interp().\n+ */\n+struct binfmt_misc_interp {\n+\tstruct list_head\tlist;\n+\tstruct file\t\t*file;\n+\tconst char\t\t*path;\n+\tchar\t\t\tname[];\n+};\n+\n+const struct binfmt_misc_interp *\n+binfmt_misc_find_interp(const struct list_head *interps, const char *name);\n+\n /**\n  * enum bpf_binprm_flags - per-exec invocation flags a load program can request\n  * @BPF_BINPRM_PRESERVE_ARGV0: keep the caller's argv[0] (like the 'P' flag)\n@@ -42,10 +72,11 @@ enum bpf_binprm_flags {\n  *         so it can read the binary to decide, but the verifier rejects\n  *         the interpreter selection kfuncs in it\n  * @load:  select an interpreter for the matched @bprm via\n- *         bpf_binprm_set_interp() and return zero; a match is committed, so\n- *         a failure fails the exec instead of falling through to later\n- *         entries; -ENOEXEC does not fail the exec but moves on to the\n- *         remaining binary formats\n+ *         bpf_binprm_set_interp(), or one the entry bound via\n+ *         bpf_binprm_select_interp(), and return zero; a match is\n+ *         committed, so a failure fails the exec instead of falling\n+ *         through to later entries; -ENOEXEC does not fail the exec but\n+ *         moves on to the remaining binary formats\n  * @name: name that 'B' entries reference the handler by\n  */\n struct binfmt_misc_ops {\ndiff --git a/include/linux/binfmts.h b/include/linux/binfmts.h\nindex a2daecbb01d6a..f686a37f7a0a0 100644\n--- a/include/linux/binfmts.h\n+++ b/include/linux/binfmts.h\n@@ -14,7 +14,10 @@ struct coredump_params;\n \n /* Interpreter selection staged by a bpf binfmt_misc handler. */\n struct binfmt_misc_bpf {\n+\t/* interpreters the matched entry bound, selectable by name */\n+\tconst struct list_head *bpf_interps;\n \tconst char *bpf_interp;\t\t/* interpreter selected by a bpf handler */\n+\tstruct file *bpf_interp_file;\t/* the bound interpreter it selected */\n \tconst char *bpf_interp_arg;\t/* interpreter argument from a bpf handler */\n \tu64 bpf_flags;\t\t\t/* enum bpf_binprm_flags from a bpf handler */\n };\ndiff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile\nindex 390fe11a7bedf..410c93606a0c3 100644\n--- a/tools/testing/selftests/exec/Makefile\n+++ b/tools/testing/selftests/exec/Makefile\n@@ -25,6 +25,10 @@ TEST_GEN_PROGS += check-exec\n # or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.\n TEST_GEN_PROGS += binfmt_misc_selfpin\n \n+# 'D' (register disabled) binfmt_misc test: an entry that exists but does\n+# not dispatch until it is enabled. Static magic entry, no bpf toolchain.\n+TEST_GEN_PROGS += binfmt_misc_disabled\n+\n # Static ('T' flag) transparent binfmt_misc test; the asserting interpreter\n # is shared with the bpf harness's transparent case. No bpf toolchain needed.\n TEST_GEN_PROGS += binfmt_misc_transparent\n@@ -52,8 +56,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) \u003e/dev/null 2\u003e\u00261 \u0026\u0026 \\\n ifeq ($(HAVE_BPF_TOOLCHAIN),y)\n TEST_GEN_PROGS += binfmt_misc_bpf\n TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o\n-TEST_GEN_FILES += loader.bpf.o\n-TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app\n+TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o\n+TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp\n else\n $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)\n endif\n@@ -123,6 +127,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h\n $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c\n \t$(CC) $(CFLAGS) $(LDFLAGS) $\u003c -o $@\n \n+$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c\n+\t$(CC) $(CFLAGS) $(LDFLAGS) $\u003c -o $@\n+\n $(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h\n \t$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $\u003c -o $@\n \ndiff --git a/tools/testing/selftests/exec/binfmt_bind_interp.c b/tools/testing/selftests/exec/binfmt_bind_interp.c\nnew file mode 100644\nindex 0000000000000..06d65062856b7\n--- /dev/null\n+++ b/tools/testing/selftests/exec/binfmt_bind_interp.c\n@@ -0,0 +1,14 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Test interpreter for the bound-interpreter case of the binfmt_misc_bpf\n+ * selftest. Two copies are installed at different paths and bound to one\n+ * entry under different names; printing argv[0] - the path the kernel ran\n+ * this copy under - tells the harness which of them the load program picked.\n+ */\n+#include \u003cstdio.h\u003e\n+\n+int main(int argc, char **argv)\n+{\n+\tprintf(\"BIND_RAN %s\\n\", argc \u003e 0 ? argv[0] : \"\");\n+\treturn 0;\n+}\ndiff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c\nindex 069768a66ba07..2c7b63075f1d9 100644\n--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c\n+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c\n@@ -9,7 +9,7 @@\n  *\n  *     echo ':name:B::::\u003chandler\u003e:' \u003e /proc/sys/fs/binfmt_misc/register\n  *\n- * Three self-contained cases are exercised:\n+ * Five self-contained cases are exercised:\n  *\n  *   1. bpf_interp: the match program matches a synthetic aarch64 ELF header\n  *      from the prefetched bprm-\u003ebuf and the load program routes it to a\n@@ -26,6 +26,11 @@\n  *      (binfmt_loader_payload) runs as the main image with the selected\n  *      interpreter substituted for its PT_INTERP and asserts the native\n  *      identity from inside.\n+ *   5. interp_bind: an entry registered disabled with 'D' is given its\n+ *      interpreters one write at a time, and the load program picks one by\n+ *      name per exec. Replacing what the path holds afterwards changes\n+ *      nothing, which is the point of binding a file rather than resolving\n+ *      a name at exec time. Enabling the entry seals it.\n  *\n  * The first two route to a test interpreter that prints BPF_INTERP_RAN,\n  * proving the program's chosen interpreter actually ran.\n@@ -54,6 +59,12 @@\n #define TRANS_EXPECT\t\"TRANSPARENT_OK\"\n #define LOADER_INTERP\t\"/tmp/binfmt_loader_interp\"\n #define LOADER_PATH\t\"/tmp/binfmt_bpf_loader.ldrtest\"\n+#define BIND_FIRST\t\"/tmp/binfmt_bind_first\"\n+#define BIND_SECOND\t\"/tmp/binfmt_bind_second\"\n+#define BIND_ARM_PATH\t\"/tmp/binfmt_bind_arm\"\n+#define BIND_RISCV_PATH\t\"/tmp/binfmt_bind_riscv\"\n+#define BIND_EXPECT\t\"BIND_RAN \"\n+#define BIND_MAX\t100\n \n /* A minimal 64-bit little-endian ELF header, padded to the read size. */\n static int create_fake_elf(const char *path, unsigned short machine)\n@@ -82,11 +93,17 @@ static int create_fake_elf(const char *path, unsigned short machine)\n \treturn 0;\n }\n \n-static int register_entry(const char *name, const char *handler)\n+/*\n+ * Register a 'B' entry for @handler. With @flags \"D\" the entry is created\n+ * disabled, which is what leaves it open to being given interpreters.\n+ */\n+static int register_entry(const char *name, const char *handler,\n+\t\t\t  const char *flags)\n {\n \tchar rule[PATH_MAX];\n \n-\tsnprintf(rule, sizeof(rule), \":%s:B::::%s:\", name, handler);\n+\tsnprintf(rule, sizeof(rule), \":%s:B::::%s:%s\", name, handler,\n+\t\t flags ? flags : \"\");\n \treturn write_reg(rule);\n }\n \n@@ -106,6 +123,30 @@ static int check_output(const char *cmd, const char *expected)\n \treturn strncmp(buf, expected, strlen(expected)) ? -1 : 0;\n }\n \n+/* Does the kernel BTF know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF)? */\n+static bool have_binfmt_misc_ops(void)\n+{\n+\tstruct btf *btf = btf__load_vmlinux_btf();\n+\tbool have;\n+\n+\thave = btf \u0026\u0026 btf__find_by_name_kind(btf, \"binfmt_misc_ops\",\n+\t\t\t\t\t     BTF_KIND_STRUCT) \u003e= 0;\n+\tbtf__free(btf);\n+\treturn have;\n+}\n+\n+/* The reason bpf handler cases cannot run here, NULL if they can. */\n+static const char *bpf_handler_unsupported(void)\n+{\n+\tif (getuid() != 0)\n+\t\treturn \"test must be run as root\";\n+\tif (!have_binfmt_misc_ops())\n+\t\treturn \"no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)\";\n+\tif (!binfmt_misc_available())\n+\t\treturn \"no binfmt_misc\";\n+\treturn NULL;\n+}\n+\n /* An attached handler with its 'B' entry activated. */\n struct bpf_case {\n \tstruct bpf_object *obj;\n@@ -115,10 +156,12 @@ struct bpf_case {\n \n /*\n  * Load @objfile, attach its struct_ops map @handler (which publishes the\n- * handler) and activate a 'B' entry named @entry that references it.\n+ * handler) and register a 'B' entry named @entry that references it, with\n+ * @flags as the entry's register-string flags.\n  */\n-static int bpf_case_start(struct bpf_case *c, const char *objfile,\n-\t\t\t  const char *handler, const char *entry)\n+static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,\n+\t\t\t\tconst char *handler, const char *entry,\n+\t\t\t\tconst char *flags)\n {\n \tstruct bpf_map *map;\n \n@@ -148,7 +191,7 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,\n \t\tc-\u003elink = NULL;\n \t\tgoto fail;\n \t}\n-\tif (register_entry(entry, handler)) {\n+\tif (register_entry(entry, handler, flags)) {\n \t\tfprintf(stderr, \"register 'B' entry '%s' failed\\n\", entry);\n \t\tgoto fail;\n \t}\n@@ -162,6 +205,12 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,\n \treturn -1;\n }\n \n+static int bpf_case_start(struct bpf_case *c, const char *objfile,\n+\t\t\t  const char *handler, const char *entry)\n+{\n+\treturn bpf_case_start_flags(c, objfile, handler, entry, NULL);\n+}\n+\n static void bpf_case_stop(struct bpf_case *c)\n {\n \tunregister(c-\u003eentry);\n@@ -190,23 +239,10 @@ FIXTURE(bpf_handler) {\n FIXTURE_SETUP(bpf_handler)\n {\n \tchar src[PATH_MAX];\n-\tstruct btf *btf;\n-\n-\tif (getuid() != 0)\n-\t\tSKIP(return, \"test must be run as root\");\n-\n-\t/* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */\n-\tbtf = btf__load_vmlinux_btf();\n-\tif (!btf || btf__find_by_name_kind(btf, \"binfmt_misc_ops\",\n-\t\t\t\t\t   BTF_KIND_STRUCT) \u003c 0) {\n-\t\tbtf__free(btf);\n-\t\tSKIP(return,\n-\t\t     \"no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)\");\n-\t}\n-\tbtf__free(btf);\n+\tconst char *why = bpf_handler_unsupported();\n \n-\tif (!binfmt_misc_available())\n-\t\tSKIP(return, \"no binfmt_misc\");\n+\tif (why)\n+\t\tSKIP(return, \"%s\", why);\n \n \t/* Shared test interpreter. */\n \tASSERT_EQ(artifact_path(src, sizeof(src), \"binfmt_bpf_interp\"), 0);\n@@ -258,7 +294,7 @@ TEST_F(bpf_handler, transparent_dispatch)\n \tchar src[PATH_MAX], cmd[PATH_MAX + 16];\n \n \t/* Probe for transparent-mode support via its static counterpart. */\n-\tif (binfmt_flag_supported('T'))\n+\tif (!binfmt_flag_supported('T'))\n \t\tSKIP(return, \"kernel without transparent mode\");\n \n \tASSERT_EQ(artifact_path(src, sizeof(src), \"binfmt_transparent_interp\"), 0);\n@@ -307,4 +343,226 @@ TEST_F(bpf_handler, loader_substitution)\n \tunlink(LOADER_INTERP);\n }\n \n+/* The errno an exec of @path fails with, 0 if it succeeded. */\n+static int exec_errno(const char *path)\n+{\n+\tint status;\n+\tpid_t pid;\n+\n+\tpid = fork();\n+\tif (pid == 0) {\n+\t\texecl(path, path, (char *)NULL);\n+\t\t_exit(errno);\n+\t}\n+\tif (pid \u003c 0 || waitpid(pid, \u0026status, 0) != pid || !WIFEXITED(status))\n+\t\treturn -1;\n+\treturn WEXITSTATUS(status);\n+}\n+\n+/* Install a copy of the bound-interpreter test binary at @path. */\n+static int install_interp(const char *path)\n+{\n+\tchar src[PATH_MAX];\n+\n+\tif (artifact_path(src, sizeof(src), \"binfmt_bind_interp\"))\n+\t\treturn -1;\n+\treturn copy_file(src, path);\n+}\n+\n+/* Bind @path to @entry under @name, the '+' command of a disabled entry. */\n+static int entry_bind(const char *entry, const char *name, const char *path)\n+{\n+\tchar cmd[PATH_MAX];\n+\n+\tsnprintf(cmd, sizeof(cmd), \"+%s %s\\n\", name, path);\n+\treturn entry_command(entry, cmd);\n+}\n+\n+FIXTURE(bound_interp) {\n+\tchar obj[PATH_MAX];\n+\tstruct bpf_case c;\n+\tbool started;\n+};\n+\n+FIXTURE_SETUP(bound_interp)\n+{\n+\tconst char *why = bpf_handler_unsupported();\n+\n+\tif (why)\n+\t\tSKIP(return, \"%s\", why);\n+\tif (!binfmt_flag_supported('D')) {\n+\t\tASSERT_EQ(errno, EINVAL);\n+\t\tSKIP(return, \"kernel without the 'D' flag\");\n+\t}\n+\n+\tASSERT_EQ(install_interp(BIND_FIRST), 0);\n+\tASSERT_EQ(install_interp(BIND_SECOND), 0);\n+\n+\tASSERT_EQ(artifact_path(self-\u003eobj, sizeof(self-\u003eobj),\n+\t\t\t\t\"interp_bind.bpf.o\"), 0);\n+\n+\t/*\n+\t * Registered disabled, so it cannot be matched yet and can still be\n+\t * given interpreters. Each path is resolved once, by its write(2);\n+\t * from here on the entry holds the files themselves.\n+\t */\n+\tASSERT_EQ(bpf_case_start_flags(\u0026self-\u003ec, self-\u003eobj, \"interp_bind\",\n+\t\t\t\t       \"test_interp_bind\", \"D\"), 0);\n+\tself-\u003estarted = true;\n+\n+\tASSERT_EQ(entry_bind(\"test_interp_bind\", \"first\", BIND_FIRST), 0);\n+\tASSERT_EQ(entry_bind(\"test_interp_bind\", \"second\", BIND_SECOND), 0);\n+}\n+\n+FIXTURE_TEARDOWN(bound_interp)\n+{\n+\tif (self-\u003estarted)\n+\t\tbpf_case_stop(\u0026self-\u003ec);\n+\tunlink(BIND_FIRST);\n+\tunlink(BIND_SECOND);\n+\tunlink(AARCH64_PATH);\n+\tunlink(BIND_RISCV_PATH);\n+\tunlink(BIND_ARM_PATH);\n+}\n+\n+/* Enabling is what makes the configured entry matchable. */\n+static int activate(const char *entry)\n+{\n+\treturn entry_command(entry, \"1\\n\");\n+}\n+\n+/* One entry, one interpreter per guest architecture, picked per exec. */\n+TEST_F(bound_interp, selects_by_name)\n+{\n+\tASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);\n+\tASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);\n+\n+\t/* Disabled, so it does not match and no format claims the binary. */\n+\tEXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);\n+\n+\tASSERT_EQ(activate(\"test_interp_bind\"), 0);\n+\tEXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);\n+\tEXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);\n+}\n+\n+/* What was bound is what runs, whatever the path holds afterwards. */\n+TEST_F(bound_interp, path_no_longer_decides)\n+{\n+\tchar other[PATH_MAX];\n+\n+\tASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);\n+\tASSERT_EQ(activate(\"test_interp_bind\"), 0);\n+\n+\t/* Bound interpreters are pinned against writes, exactly like 'F'. */\n+\tEXPECT_TRUE(write_denied(BIND_FIRST));\n+\n+\t/* Replace the path with a different binary: a new file, new inode. */\n+\tASSERT_EQ(artifact_path(other, sizeof(other), \"binfmt_bpf_interp\"), 0);\n+\tASSERT_EQ(unlink(BIND_FIRST), 0);\n+\tASSERT_EQ(copy_file(other, BIND_FIRST), 0);\n+\n+\tEXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);\n+}\n+\n+/* The entry reports what it bound, under the names it bound them as. */\n+TEST_F(bound_interp, entry_reports_bindings)\n+{\n+\tEXPECT_TRUE(entry_shows(\"test_interp_bind\",\n+\t\t\t\t\"bpf-interpreter first \" BIND_FIRST));\n+\tEXPECT_TRUE(entry_shows(\"test_interp_bind\",\n+\t\t\t\t\"bpf-interpreter second \" BIND_SECOND));\n+}\n+\n+/* Selecting a name the entry did not bind fails the exec. */\n+TEST_F(bound_interp, unbound_name_fails)\n+{\n+\tASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);\n+\tASSERT_EQ(activate(\"test_interp_bind\"), 0);\n+\n+\tEXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);\n+}\n+\n+/* Activating seals it: what can be matched cannot be changed. */\n+TEST_F(bound_interp, sealed_once_active)\n+{\n+\tASSERT_EQ(activate(\"test_interp_bind\"), 0);\n+\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"third\", BIND_SECOND), -EBUSY);\n+\tEXPECT_FALSE(entry_shows(\"test_interp_bind\",\n+\t\t\t\t \"bpf-interpreter third \" BIND_SECOND));\n+}\n+\n+/* The seal is for good: disabling the entry again reopens nothing. */\n+TEST_F(bound_interp, disable_does_not_unseal)\n+{\n+\tASSERT_EQ(activate(\"test_interp_bind\"), 0);\n+\tASSERT_EQ(entry_command(\"test_interp_bind\", \"0\\n\"), 0);\n+\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"third\", BIND_SECOND), -EBUSY);\n+}\n+\n+/* An entry registered without 'D' is sealed from the start. */\n+TEST_F(bound_interp, born_sealed)\n+{\n+\t/* A second entry for the handler the fixture already published. */\n+\tASSERT_EQ(register_entry(\"test_born_sealed\", \"interp_bind\", NULL), 0);\n+\n+\tEXPECT_EQ(entry_bind(\"test_born_sealed\", \"first\", BIND_FIRST), -EBUSY);\n+\tunregister(\"test_born_sealed\");\n+}\n+\n+/* A name is bound once; a second use of it is refused. */\n+TEST_F(bound_interp, duplicate_name_refused)\n+{\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"first\", BIND_SECOND), -EEXIST);\n+}\n+\n+/* A name is a printable word: the entry file reports 'name path' lines. */\n+TEST_F(bound_interp, name_must_be_printable)\n+{\n+\t/* A control character would forge a line into the entry file. */\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"a\\tb\", BIND_FIRST), -EINVAL);\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"a\\nb\", BIND_FIRST), -EINVAL);\n+\n+\t/* A space cannot even be spelled: the path starts after the first one. */\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"a b\", BIND_FIRST), -EINVAL);\n+}\n+\n+/* The command ends at the write: bytes past an embedded nul are refused. */\n+TEST_F(bound_interp, trailing_bytes_refused)\n+{\n+\tchar cmd[PATH_MAX];\n+\tsize_t len;\n+\tint fd;\n+\n+\t/* entry_command() cannot spell a nul, so write the buffer raw. */\n+\tsnprintf(cmd, sizeof(cmd), \"+nul %s\", BIND_FIRST);\n+\tlen = strlen(cmd) + 1;\n+\tmemcpy(cmd + len, \"junk\", sizeof(\"junk\"));\n+\tlen += sizeof(\"junk\");\n+\n+\tfd = open(BINFMT_DIR \"/test_interp_bind\", O_WRONLY | O_CLOEXEC);\n+\tASSERT_GE(fd, 0);\n+\tEXPECT_EQ(write(fd, cmd, len), -1);\n+\tEXPECT_EQ(errno, EINVAL);\n+\tclose(fd);\n+\n+\tEXPECT_FALSE(entry_shows(\"test_interp_bind\",\n+\t\t\t\t \"bpf-interpreter nul \" BIND_FIRST));\n+}\n+\n+/* An entry binds at most BIND_MAX interpreters. */\n+TEST_F(bound_interp, capped_bindings)\n+{\n+\tchar name[16];\n+\tint i;\n+\n+\t/* The fixture bound \"first\" and \"second\" already. */\n+\tfor (i = 2; i \u003c BIND_MAX; i++) {\n+\t\tsnprintf(name, sizeof(name), \"n%d\", i);\n+\t\tASSERT_EQ(entry_bind(\"test_interp_bind\", name, BIND_FIRST), 0);\n+\t}\n+\tEXPECT_EQ(entry_bind(\"test_interp_bind\", \"over\", BIND_FIRST), -ENOSPC);\n+}\n+\n TEST_HARNESS_MAIN\ndiff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h\nindex c6900ded019fc..745aff84dc787 100644\n--- a/tools/testing/selftests/exec/binfmt_misc_common.h\n+++ b/tools/testing/selftests/exec/binfmt_misc_common.h\n@@ -93,6 +93,45 @@ static inline void unregister(const char *name)\n \t}\n }\n \n+/* Write @line to @entry's file, reporting the errno it was refused with. */\n+static inline int entry_command(const char *entry, const char *line)\n+{\n+\tchar path[PATH_MAX];\n+\tint fd, retval = 0;\n+\tsize_t len = strlen(line);\n+\n+\tsnprintf(path, sizeof(path), BINFMT_DIR \"/%s\", entry);\n+\tfd = open(path, O_WRONLY | O_CLOEXEC);\n+\tif (fd \u003c 0)\n+\t\treturn -errno;\n+\tif (write(fd, line, len) != (ssize_t)len)\n+\t\tretval = -errno;\n+\tclose(fd);\n+\treturn retval;\n+}\n+\n+/* Does @entry's file report @line? */\n+static inline bool entry_shows(const char *entry, const char *line)\n+{\n+\tchar path[PATH_MAX], buf[PATH_MAX];\n+\tbool found = false;\n+\tFILE *fp;\n+\n+\tsnprintf(path, sizeof(path), BINFMT_DIR \"/%s\", entry);\n+\tfp = fopen(path, \"r\");\n+\tif (!fp)\n+\t\treturn false;\n+\twhile (fgets(buf, sizeof(buf), fp)) {\n+\t\tbuf[strcspn(buf, \"\\n\")] = '\\0';\n+\t\tif (!strcmp(buf, line)) {\n+\t\t\tfound = true;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tfclose(fp);\n+\treturn found;\n+}\n+\n /* Mount binfmt_misc unless it already is, and report whether it is usable. */\n static inline bool binfmt_misc_available(void)\n {\n@@ -117,16 +156,16 @@ static inline int artifact_path(char *out, size_t sz, const char *name)\n }\n \n /* Probe kernel support for a registration flag with a throwaway entry. */\n-static inline int binfmt_flag_supported(char flag)\n+static inline bool binfmt_flag_supported(char flag)\n {\n \tchar rule[64];\n \n \tsnprintf(rule, sizeof(rule), \":bm_flag_probe:E::bmprobe::/bin/true:%c\",\n \t\t flag);\n \tif (write_reg(rule))\n-\t\treturn -1;\n+\t\treturn false;\n \tunregister(\"bm_flag_probe\");\n-\treturn 0;\n+\treturn true;\n }\n \n /*\ndiff --git a/tools/testing/selftests/exec/binfmt_misc_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c\nnew file mode 100644\nindex 0000000000000..47c9e8a4ee42a\n--- /dev/null\n+++ b/tools/testing/selftests/exec/binfmt_misc_disabled.c\n@@ -0,0 +1,172 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Test the 'D' (register disabled) flag of binfmt_misc. An entry\n+ * registered with it exists but cannot be matched until userspace enables\n+ * it, which splits a registration into create and activate.\n+ *\n+ * Needs root for the registration; no bpf toolchain involved.\n+ */\n+#define _GNU_SOURCE\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+\n+#include \"binfmt_misc_common.h\"\n+#include \"kselftest_harness.h\"\n+\n+#define MAGIC\t\t\"#DISABLED-SELFTEST#\"\n+#define TARGET_PATH\t\"/tmp/binfmt_disabled_target\"\n+#define INTERP_PATH\t\"/tmp/binfmt_disabled_interp.sh\"\n+#define ENTRY\t\t\"test_disabled\"\n+#define RULE(flags)\t\":\" ENTRY \":M:0:\" MAGIC \"::\" INTERP_PATH \":\" flags\n+\n+/* The interpreter exits with a code the harness can recognise. */\n+#define EXIT_INTERP\t7\n+\n+/* The target only has to carry the magic; it is never actually loaded. */\n+static int create_target(void)\n+{\n+\tchar buf[128] = MAGIC \"\\n\";\n+\tint fd;\n+\n+\tunlink(TARGET_PATH);\n+\tfd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tif (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {\n+\t\tclose(fd);\n+\t\treturn -1;\n+\t}\n+\tclose(fd);\n+\treturn 0;\n+}\n+\n+static int create_interp(void)\n+{\n+\tchar buf[64];\n+\tint fd;\n+\n+\tunlink(INTERP_PATH);\n+\tfd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tsnprintf(buf, sizeof(buf), \"#!/bin/sh\\nexit %d\\n\", EXIT_INTERP);\n+\tif (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {\n+\t\tclose(fd);\n+\t\treturn -1;\n+\t}\n+\treturn close(fd);\n+}\n+\n+FIXTURE(disabled) {\n+};\n+\n+FIXTURE_SETUP(disabled)\n+{\n+\tif (getuid() != 0)\n+\t\tSKIP(return, \"test must be run as root\");\n+\tif (!binfmt_misc_available())\n+\t\tSKIP(return, \"no binfmt_misc\");\n+\n+\t/* Skip the whole suite on a kernel that does not know 'D'. */\n+\tif (!binfmt_flag_supported('D')) {\n+\t\tASSERT_EQ(errno, EINVAL);\n+\t\tSKIP(return, \"kernel without the 'D' flag\");\n+\t}\n+\n+\tASSERT_EQ(create_interp(), 0);\n+\tASSERT_EQ(create_target(), 0);\n+}\n+\n+FIXTURE_TEARDOWN(disabled)\n+{\n+\tunregister(ENTRY);\n+\tunlink(TARGET_PATH);\n+\tunlink(INTERP_PATH);\n+}\n+\n+/* The entry exists but does not dispatch until it is enabled. */\n+TEST_F(disabled, inert_until_enabled)\n+{\n+\tASSERT_EQ(write_reg(RULE(\"D\")), 0);\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"disabled\"));\n+\n+\t/* Nothing matches it, so no binary format claims the target. */\n+\tEXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);\n+\n+\tASSERT_EQ(entry_command(ENTRY, \"1\\n\"), 0);\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"enabled\"));\n+\tEXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);\n+}\n+\n+/* Without 'D' an entry is matchable the moment it is registered. */\n+TEST_F(disabled, enabled_without_the_flag)\n+{\n+\tASSERT_EQ(write_reg(RULE(\"\")), 0);\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"enabled\"));\n+\tEXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);\n+}\n+\n+/* 'D' is spent on the registration: the entry does not report it back. */\n+TEST_F(disabled, flag_not_reported)\n+{\n+\tASSERT_EQ(write_reg(RULE(\"D\")), 0);\n+\tEXPECT_FALSE(entry_shows(ENTRY, \"flags: D\"));\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"flags: \"));\n+}\n+\n+/* A disabled entry can be disabled and enabled like any other. */\n+TEST_F(disabled, toggles_like_any_entry)\n+{\n+\tASSERT_EQ(write_reg(RULE(\"D\")), 0);\n+\n+\tASSERT_EQ(entry_command(ENTRY, \"1\\n\"), 0);\n+\tASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);\n+\tASSERT_EQ(entry_command(ENTRY, \"0\\n\"), 0);\n+\tEXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);\n+\tASSERT_EQ(entry_command(ENTRY, \"1\\n\"), 0);\n+\tEXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);\n+}\n+\n+/* 'D' composes with the invocation flags a static entry can carry. */\n+TEST_F(disabled, composes_with_invocation_flags)\n+{\n+\tASSERT_EQ(write_reg(RULE(\"PD\")), 0);\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"disabled\"));\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"flags: P\"));\n+}\n+\n+/* '-1' to the status file sweeps a staged entry with everything else. */\n+TEST_F(disabled, removed_by_remove_all)\n+{\n+\tint fd;\n+\n+\tASSERT_EQ(write_reg(RULE(\"D\")), 0);\n+\tEXPECT_TRUE(entry_shows(ENTRY, \"disabled\"));\n+\n+\tfd = open(BINFMT_DIR \"/status\", O_WRONLY | O_CLOEXEC);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(write(fd, \"-1\", 2), 2);\n+\tclose(fd);\n+\n+\tEXPECT_NE(access(BINFMT_DIR \"/\" ENTRY, F_OK), 0);\n+}\n+\n+/* A file handle held across a removal cannot resurrect the entry. */\n+TEST_F(disabled, no_resurrection_after_remove)\n+{\n+\tint fd;\n+\n+\tASSERT_EQ(write_reg(RULE(\"D\")), 0);\n+\tfd = open(BINFMT_DIR \"/\" ENTRY, O_WRONLY | O_CLOEXEC);\n+\tASSERT_GE(fd, 0);\n+\n+\tASSERT_EQ(write(fd, \"-1\", 2), 2);\n+\tEXPECT_NE(access(BINFMT_DIR \"/\" ENTRY, F_OK), 0);\n+\n+\t/* Accepted like any toggle of a removed entry, but publishes nothing. */\n+\tEXPECT_EQ(write(fd, \"1\", 1), 1);\n+\tEXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);\n+\tclose(fd);\n+}\n+\n+TEST_HARNESS_MAIN\ndiff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c\nindex d0cb845df1d30..2ebf73de8018c 100644\n--- a/tools/testing/selftests/exec/binfmt_misc_transparent.c\n+++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c\n@@ -56,7 +56,7 @@ FIXTURE_SETUP(transparent)\n \tASSERT_EQ(create_target(), 0);\n \n \t/* Skip the whole suite on a kernel that does not know 'T'. */\n-\tif (binfmt_flag_supported('T')) {\n+\tif (!binfmt_flag_supported('T')) {\n \t\tASSERT_EQ(errno, EINVAL);\n \t\tSKIP(return, \"kernel without the 'T' flag\");\n \t}\ndiff --git a/tools/testing/selftests/exec/interp_bind.bpf.c b/tools/testing/selftests/exec/interp_bind.bpf.c\nnew file mode 100644\nindex 0000000000000..1ce45cca215f0\n--- /dev/null\n+++ b/tools/testing/selftests/exec/interp_bind.bpf.c\n@@ -0,0 +1,76 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * binfmt_misc_ops handler for the selftest's bound-interpreter case: one\n+ * handler, one entry, an interpreter per guest architecture - each bound to\n+ * a file when the entry was registered rather than to a path resolved at\n+ * exec time. The load program names the one it wants; a name the entry did\n+ * not bind fails the exec, which the harness checks too.\n+ */\n+#include \"vmlinux.h\"\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+#define EI_CLASS\t4\n+#define ELFCLASS64\t2\n+#define E_MACHINE_OFF\t18\n+#define EM_ARM\t\t40\n+#define EM_AARCH64\t183\n+#define EM_RISCV\t243\n+\n+extern int bpf_binprm_select_interp(struct linux_binprm *bprm,\n+\t\t\t\t    const char *name, size_t name__sz) __ksym;\n+\n+/* The guest architecture of a 64-bit ELF, or zero if it is not one. */\n+static __u16 elf_machine(struct linux_binprm *bprm)\n+{\n+\tif (bprm-\u003ebuf[0] != 0x7f || bprm-\u003ebuf[1] != 'E' ||\n+\t    bprm-\u003ebuf[2] != 'L' || bprm-\u003ebuf[3] != 'F' ||\n+\t    bprm-\u003ebuf[EI_CLASS] != ELFCLASS64)\n+\t\treturn 0;\n+\n+\t/* Little-endian 16-bit field, read byte-wise for the verifier. */\n+\treturn (__u8)bprm-\u003ebuf[E_MACHINE_OFF] |\n+\t       ((__u16)(__u8)bprm-\u003ebuf[E_MACHINE_OFF + 1] \u003c\u003c 8);\n+}\n+\n+SEC(\"struct_ops.s/match\")\n+bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm)\n+{\n+\t__u16 machine = elf_machine(bprm);\n+\n+\treturn machine == EM_AARCH64 || machine == EM_RISCV ||\n+\t       machine == EM_ARM;\n+}\n+\n+SEC(\"struct_ops.s/load\")\n+int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)\n+{\n+\t/*\n+\t * Names, not paths: each one selects a file the entry pre-opened, so\n+\t * nothing is resolved here or later, in any namespace. The buffers\n+\t * are on the stack because the verifier rejects .rodata for a sized\n+\t * memory argument.\n+\t */\n+\tchar first[] = \"first\";\n+\tchar second[] = \"second\";\n+\tchar unbound[] = \"unbound\";\n+\n+\tswitch (elf_machine(bprm)) {\n+\tcase EM_AARCH64:\n+\t\treturn bpf_binprm_select_interp(bprm, first, sizeof(first));\n+\tcase EM_RISCV:\n+\t\treturn bpf_binprm_select_interp(bprm, second, sizeof(second));\n+\t}\n+\n+\t/* The entry bound nothing under this name: -ENOENT fails the exec. */\n+\treturn bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct binfmt_misc_ops interp_bind = {\n+\t.match\t= (void *)interp_bind_match,\n+\t.load\t= (void *)interp_bind_load,\n+\t.name\t= \"interp_bind\",\n+};\n"
}

2/1 2026/08/01 02:56 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "BINFMT_MISC",
    "BINFMT_MISC_BPF"
  ],
  "FocusSymbols": [
    "binfmt_misc_find_interp",
    "bpf_binprm_select_interp",
    "bpf_binprm_set_interp",
    "load_misc_binary",
    "bm_entry_write",
    "bm_register_write"
  ],
  "Reasoning": "The patch introduces a new feature to binfmt_misc allowing BPF handlers to bind interpreters to an entry and select them by name at exec time. This modifies core kernel logic in fs/binfmt_misc.c and fs/binfmt_misc_bpf.c and is reachable from userspace via binfmt_misc configuration and execution of matching binaries.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit df9251a60da166224bea9baa3bd9c42f74b9d5ca
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Aug 1 02:56:01 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index c80702b4ccd5f..3e2b2a9415bc8 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:
@@ -186,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
@@ -224,8 +286,11 @@ 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`` 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
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 707f8a14f8a63..ad8c4f64bf102 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -24,6 +24,8 @@
 #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>
 #include <linux/printk.h>
@@ -37,6 +39,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 +56,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 +88,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. */
@@ -96,9 +110,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 */
@@ -175,7 +189,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;
 
@@ -216,6 +235,108 @@ 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)
+{
+	if (IS_ERR_OR_NULL(f))
+		return;
+	exe_file_allow_write_access(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
+ * 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 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
+ *
+ * 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 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
+ */
+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;
+
+	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);
+	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;
+	/* 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;
+}
+
 static void bm_entry_free_rcu(struct rcu_head *rcu)
 {
 	struct binfmt_misc_entry *e = container_of(rcu, struct binfmt_misc_entry, rcu);
@@ -232,21 +353,36 @@ 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))
+
+/* 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
@@ -281,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
@@ -291,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)
@@ -317,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);
 }
 
@@ -364,23 +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);
+	}
 
-	interp_file = file_clone_open(e->interp_file);
+	interp_file = file_clone_open(bound);
 	if (IS_ERR(interp_file))
 		return interp_file;
 
@@ -490,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. */
@@ -512,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);
 
@@ -684,7 +837,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);
@@ -701,6 +854,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);
 
@@ -780,18 +934,30 @@ 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.
+	 * 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) && 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.
+	 * 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;
+		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))
@@ -814,6 +980,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);
 }
 
@@ -825,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')
@@ -850,8 +1017,26 @@ 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. */
+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++)
@@ -867,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: ");
@@ -992,10 +1190,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 +1203,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);
 }
 
@@ -1056,20 +1263,138 @@ 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:
 		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, 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);
+		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;
@@ -1106,16 +1431,18 @@ 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;
 
 	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;
 }
@@ -1123,9 +1450,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);
@@ -1142,33 +1468,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);
@@ -1197,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 92003ea640d3f..b246d886437b8 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 856731f78d054..a14f28b15607a 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 4abdfd36b3fa8..072e4b3dd78d2 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 a2daecbb01d6a..f686a37f7a0a0 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 */
 };
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 390fe11a7bedf..410c93606a0c3 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
@@ -52,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
@@ -123,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 0000000000000..06d65062856b7
--- /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 069768a66ba07..2c7b63075f1d9 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);
 }
 
@@ -106,6 +123,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;
@@ -115,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;
 
@@ -148,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;
 	}
@@ -162,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);
@@ -190,23 +239,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");
-
-	/* 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);
+	const char *why = bpf_handler_unsupported();
 
-	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);
@@ -258,7 +294,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);
@@ -307,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/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
index c6900ded019fc..745aff84dc787 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)
 {
@@ -117,16 +156,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_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c
new file mode 100644
index 0000000000000..47c9e8a4ee42a
--- /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
diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c
index d0cb845df1d30..2ebf73de8018c 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");
 	}
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 0000000000000..1ce45cca215f0
--- /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",
+};

3/2 2026/08/01 02:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25524
							output: 119
							thoughts: 664
						

Thoughts:
**Fuzzing Plan for Binfmt_misc Interpreter Binding**

Okay, so we've got a new patch that introduces interpreter binding to `binfmt_misc`. This is a pretty significant change, touching core kernel logic in `fs/binfmt_misc.c`, `fs/binfmt_misc_bpf.c`, and `fs/exec.c`. It's definitely functional and we need to fuzz it thoroughly. This patch introduces a "D" flag for disabled entries which can have an interpreter bound later with the "+name path" feature, then be selected by a BPF handler using a new `bpf_binprm_select_interp()` kfunc. That's the core of it, allowing BPF handlers to choose an interpreter without doing path resolution at execution time.

Since it’s in the core kernel, fuzzing is essential here. My immediate focus is identifying the key functions to target. Let's see... looking at `fs/binfmt_misc.c`, I've got:

*   `binfmt_misc_find_interp`
*   `entry_attach_interpreter` (static)
*   `bm_entry_add_interp` (static)
*   `bm_entry_write` (static)
*   `create_entry`
*   `add_entry`
*   `bm_register_write`
*   `entry_select_interpreter`
*   `entry_open_interpreter`

And from `fs/binfmt_misc_bpf.c`:

*   `bpf_binprm_select_interp` (kfunc, non-static)
*   `bpf_binprm_set_interp` (kfunc, modified)

Alright, `binfmt_misc_find_interp`, `bpf_binprm_select_interp`, and `bpf_binprm_set_interp` seem immediately relevant as non-static functions. `bm_entry_write` and `bm_register_write` are file operations, so even though they are static, they can still be reached via syscalls, therefore are important. However, `load_misc_binary` is likely the main entrypoint into this whole module so we'll need to consider it too.

To make sure I have the exact names and signatures, I'm going to run `default_api:grepper` to get the correct information. Let's make sure we hit every angle of this.



4/2 2026/08/01 02:56 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "^(int|ssize_t|void|struct file \\*|const struct binfmt_misc_interp \\*|const char \\*).* (bm_entry_write|bm_register_write|binfmt_misc_find_interp|bpf_binprm_select_interp|bpf_binprm_set_interp|load_misc_binary|entry_select_interpreter|entry_open_interpreter|bm_entry_add_interp)\\("
}

Results:
{
  "error": "no matches"
}

5/2 2026/08/01 02:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 26866
							output: 88
							thoughts: 8
						

Thoughts:
Okay, here's my interpretation of that thought process, tailored for an expert audience, and presented as if it were my own internal monologue:

**Simplifying the Approach**

Alright, I'm going to take a step back and reassess. The current approach, while potentially powerful, might be overcomplicating things. Let's see if a more basic `grep` can achieve the desired outcome. Sometimes, brute force and simplicity yield better results than complex, layered solutions, especially for initial debugging or validation. I'll start with a straightforward pattern and iteratively refine it as needed, focusing on minimizing unnecessary complexity at this stage. It's a pragmatic shift – avoid overkill, get something functional first, then optimize from there.



6/2 2026/08/01 02:56 tool grepper
0m Args:
{
  "Expression": "(bm_entry_write|bm_register_write|binfmt_misc_find_interp|bpf_binprm_select_interp|bpf_binprm_set_interp|load_misc_binary|entry_select_interpreter|entry_open_interpreter|bm_entry_add_interp)"
}

Results:
{
  "Output": "Documentation/admin-guide/binfmt-misc.rst=194=and derive the interpreter from the binary's location. It selects the\nDocumentation/admin-guide/binfmt-misc.rst:195:interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an\nDocumentation/admin-guide/binfmt-misc.rst-196-absolute path and returning ``0``. A match is committed: a failing\n--\nDocumentation/admin-guide/binfmt-misc.rst=223=The ``load`` program then selects one per exec by name with the\nDocumentation/admin-guide/binfmt-misc.rst:224:``bpf_binprm_select_interp()`` kfunc, and every exec runs a clone of the\nDocumentation/admin-guide/binfmt-misc.rst-225-file that was opened. The path decides which file is bound and nothing\n--\nDocumentation/admin-guide/binfmt-misc.rst=255=The ``load`` program can also pass a single argument to the interpreter with\nDocumentation/admin-guide/binfmt-misc.rst:256:the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the\nDocumentation/admin-guide/binfmt-misc.rst-257-interpreter and the binary, exactly like the optional argument of a ``#!``\n--\nfs/binfmt_misc.c=103=struct binfmt_misc_entry {\n--\nfs/binfmt_misc.c-115-\tstruct list_head interps;\t/* the interpreters it bound */\nfs/binfmt_misc.c:116:\trefcount_t users;\t\t/* sync removal with load_misc_binary() */\nfs/binfmt_misc.c-117-\tstruct rcu_head rcu;\n--\nfs/binfmt_misc.c-138-\nfs/binfmt_misc.c:139:/* Protects the entry walk in load_misc_binary(), which may sleep in it. */\nfs/binfmt_misc.c-140-DEFINE_STATIC_SRCU_FAST(bm_entries_srcu);\n--\nfs/binfmt_misc.c=231=static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc,\n--\nfs/binfmt_misc.c-238-/**\nfs/binfmt_misc.c:239: * binfmt_misc_find_interp - find a bound interpreter by name\nfs/binfmt_misc.c-240- * @interps: the interpreters the matched entry was registered with\n--\nfs/binfmt_misc.c=245=const struct binfmt_misc_interp *\nfs/binfmt_misc.c:246:binfmt_misc_find_interp(const struct list_head *interps, const char *name)\nfs/binfmt_misc.c-247-{\n--\nfs/binfmt_misc.c=312=static int entry_attach_interpreter(struct binfmt_misc_entry *e,\n--\nfs/binfmt_misc.c-318-\nfs/binfmt_misc.c:319:\tif (binfmt_misc_find_interp(\u0026e-\u003einterps, name))\nfs/binfmt_misc.c-320-\t\treturn -EEXIST;\n--\nfs/binfmt_misc.c=340=static void bm_entry_free_rcu(struct rcu_head *rcu)\n--\nfs/binfmt_misc.c-353- *\nfs/binfmt_misc.c:354: * Free entry syncing with load_misc_binary() and defer final free to\nfs/binfmt_misc.c:355: * load_misc_binary() in case it is using the binary type handler we were\nfs/binfmt_misc.c-356- * requested to remove. Also the teardown for a registration that fails\n--\nfs/binfmt_misc.c=399=static struct binfmt_misc *current_binfmt_misc(void)\n--\nfs/binfmt_misc.c-414-/**\nfs/binfmt_misc.c:415: * entry_select_interpreter - get the interpreter for the matched @e\nfs/binfmt_misc.c-416- * @e: matched binary type handler\n--\nfs/binfmt_misc.c-425- */\nfs/binfmt_misc.c:426:static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,\nfs/binfmt_misc.c-427-\t\t\t\t\t    struct linux_binprm *bprm)\n--\nfs/binfmt_misc.c=477=static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,\n--\nfs/binfmt_misc.c-504-/**\nfs/binfmt_misc.c:505: * entry_open_interpreter - open the entry's interpreter for execution\nfs/binfmt_misc.c-506- * @e: matched binary type handler\n--\nfs/binfmt_misc.c-515- */\nfs/binfmt_misc.c:516:static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,\nfs/binfmt_misc.c-517-\t\t\t\t\t   struct linux_binprm *bprm,\n--\nfs/binfmt_misc.c=559=static int build_interp_argv(struct linux_binprm *bprm, const char *interpreter,\n--\nfs/binfmt_misc.c-608- */\nfs/binfmt_misc.c:609:static int load_misc_binary(struct linux_binprm *bprm)\nfs/binfmt_misc.c-610-{\n--\nfs/binfmt_misc.c-628-\nfs/binfmt_misc.c:629:\tinterpreter = entry_select_interpreter(fmt, bprm);\nfs/binfmt_misc.c-630-\tif (IS_ERR(interpreter))\n--\nfs/binfmt_misc.c-645-\tif (flags \u0026 MISC_FMT_LOADER) {\nfs/binfmt_misc.c:646:\t\tinterp_file = entry_open_interpreter(fmt, bprm, interpreter);\nfs/binfmt_misc.c-647-\t\tif (IS_ERR(interp_file)) {\n--\nfs/binfmt_misc.c-667-\nfs/binfmt_misc.c:668:\tinterp_file = entry_open_interpreter(fmt, bprm, interpreter);\nfs/binfmt_misc.c-669-\tif (IS_ERR(interp_file))\n--\nfs/binfmt_misc.c=1137=static void bm_evict_inode(struct inode *inode)\n--\nfs/binfmt_misc.c-1161- * unlink(2) happens under the exclusively held inode lock of the root\nfs/binfmt_misc.c:1162: * dentry keeping the list stable for writers. load_misc_binary() walks it\nfs/binfmt_misc.c-1163- * concurrently under SRCU. The entries_lock is only held around the actual\n--\nfs/binfmt_misc.c=1252=static int bm_entry_open(struct inode *inode, struct file *file)\n--\nfs/binfmt_misc.c-1259-\nfs/binfmt_misc.c:1260:\t/* seq_open() clears FMODE_PWRITE, bm_entry_write() takes any offset */\nfs/binfmt_misc.c-1261-\tif (file-\u003ef_mode \u0026 FMODE_WRITE)\n--\nfs/binfmt_misc.c-1273-/**\nfs/binfmt_misc.c:1274: * bm_entry_add_interp - bind another interpreter to a staged entry\nfs/binfmt_misc.c-1275- * @e: the entry\n--\nfs/binfmt_misc.c-1288- */\nfs/binfmt_misc.c:1289:static ssize_t bm_entry_add_interp(struct binfmt_misc_entry *e,\nfs/binfmt_misc.c-1290-\t\t\t\t   struct file *file, char *buf, size_t count)\n--\nfs/binfmt_misc.c-1349-\nfs/binfmt_misc.c:1350:static ssize_t bm_entry_write(struct file *file, const char __user *buffer,\nfs/binfmt_misc.c-1351-\t\t\t\tsize_t count, loff_t *ppos)\n--\nfs/binfmt_misc.c-1367-\tif (buf[0] == '+')\nfs/binfmt_misc.c:1368:\t\treturn bm_entry_add_interp(e, file, buf, count);\nfs/binfmt_misc.c-1369-\n--\nfs/binfmt_misc.c=1408=static const struct file_operations bm_entry_operations = {\n--\nfs/binfmt_misc.c-1410-\t.read\t\t= seq_read,\nfs/binfmt_misc.c:1411:\t.write\t\t= bm_entry_write,\nfs/binfmt_misc.c-1412-\t.llseek\t\t= seq_lseek,\n--\nfs/binfmt_misc.c=1419=static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb)\n--\nfs/binfmt_misc.c-1449-\nfs/binfmt_misc.c:1450:static ssize_t bm_register_write(struct file *file, const char __user *buffer,\nfs/binfmt_misc.c-1451-\t\t\t       size_t count, loff_t *ppos)\n--\nfs/binfmt_misc.c=1491=static const struct file_operations bm_register_operations = {\nfs/binfmt_misc.c:1492:\t.write\t\t= bm_register_write,\nfs/binfmt_misc.c-1493-\t.llseek\t\t= noop_llseek,\n--\nfs/binfmt_misc.c=1546=static int bm_fill_super(struct super_block *sb, struct fs_context *fc)\n--\nfs/binfmt_misc.c-1603-\t * that the binary type list for this binfmt_misc mount is empty making\nfs/binfmt_misc.c:1604:\t * load_misc_binary() return -ENOEXEC independent of whether -\u003eenabled\nfs/binfmt_misc.c-1605-\t * is true. Instead, if someone mounts binfmt_misc for the first time or\n--\nfs/binfmt_misc.c=1649=static struct linux_binfmt misc_format = {\nfs/binfmt_misc.c-1650-\t.module = THIS_MODULE,\nfs/binfmt_misc.c:1651:\t.load_binary = load_misc_binary,\nfs/binfmt_misc.c-1652-};\n--\nfs/binfmt_misc_bpf.c=119=__bpf_kfunc_start_defs();\n--\nfs/binfmt_misc_bpf.c-121-/**\nfs/binfmt_misc_bpf.c:122: * bpf_binprm_set_interp - select the interpreter for the current exec\nfs/binfmt_misc_bpf.c-123- * @bprm: binary that is being executed\n--\nfs/binfmt_misc_bpf.c-131- * returns. Calling it again replaces the selection, as does selecting an\nfs/binfmt_misc_bpf.c:132: * interpreter the entry bound with bpf_binprm_select_interp().\nfs/binfmt_misc_bpf.c-133- *\n--\nfs/binfmt_misc_bpf.c-135- */\nfs/binfmt_misc_bpf.c:136:__bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm,\nfs/binfmt_misc_bpf.c-137-\t\t\t\t      const char *path, size_t path__sz)\n--\nfs/binfmt_misc_bpf.c-160-/**\nfs/binfmt_misc_bpf.c:161: * bpf_binprm_select_interp - run this exec under an interpreter the entry bound\nfs/binfmt_misc_bpf.c-162- * @bprm: binary that is being executed\n--\nfs/binfmt_misc_bpf.c-166- * To be called from the load program of a struct binfmt_misc_ops handler\nfs/binfmt_misc_bpf.c:167: * instead of bpf_binprm_set_interp(). It selects one of the interpreters\nfs/binfmt_misc_bpf.c-168- * the matched entry was registered with, each of which was opened once when\n--\nfs/binfmt_misc_bpf.c-177- */\nfs/binfmt_misc_bpf.c:178:__bpf_kfunc int bpf_binprm_select_interp(struct linux_binprm *bprm,\nfs/binfmt_misc_bpf.c-179-\t\t\t\t\t const char *name, size_t name__sz)\n--\nfs/binfmt_misc_bpf.c-190-\nfs/binfmt_misc_bpf.c:191:\tinterp = binfmt_misc_find_interp(bprm-\u003ebpf_interps, name);\nfs/binfmt_misc_bpf.c-192-\tif (!interp)\n--\nfs/binfmt_misc_bpf.c-203-/**\nfs/binfmt_misc_bpf.c:204: * bpf_binprm_set_interp_arg - set a single argument for the interpreter\nfs/binfmt_misc_bpf.c-205- * @bprm: binary that is being executed\n--\nfs/binfmt_misc_bpf.c-215- */\nfs/binfmt_misc_bpf.c:216:__bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,\nfs/binfmt_misc_bpf.c-217-\t\t\t\t\t  const char *arg, size_t arg__sz)\n--\nfs/binfmt_misc_bpf.c=280=BTF_KFUNCS_START(bm_bpf_kfunc_ids)\nfs/binfmt_misc_bpf.c:281:BTF_ID_FLAGS(func, bpf_binprm_set_interp, KF_SLEEPABLE)\nfs/binfmt_misc_bpf.c:282:BTF_ID_FLAGS(func, bpf_binprm_select_interp, KF_SLEEPABLE)\nfs/binfmt_misc_bpf.c:283:BTF_ID_FLAGS(func, bpf_binprm_set_interp_arg, KF_SLEEPABLE)\nfs/binfmt_misc_bpf.c-284-BTF_ID_FLAGS(func, bpf_binprm_set_flags, KF_SLEEPABLE)\n--\ninclude/linux/binfmt_misc.h=10=struct user_namespace;\n--\ninclude/linux/binfmt_misc.h-30- * is handed to the handler's load program for the duration of one exec,\ninclude/linux/binfmt_misc.h:31: * which picks one with bpf_binprm_select_interp().\ninclude/linux/binfmt_misc.h-32- */\n--\ninclude/linux/binfmt_misc.h=40=const struct binfmt_misc_interp *\ninclude/linux/binfmt_misc.h:41:binfmt_misc_find_interp(const struct list_head *interps, const char *name);\ninclude/linux/binfmt_misc.h-42-\n--\ninclude/linux/binfmt_misc.h=59=enum bpf_binprm_flags {\n--\ninclude/linux/binfmt_misc.h-74- * @load:  select an interpreter for the matched @bprm via\ninclude/linux/binfmt_misc.h:75: *         bpf_binprm_set_interp(), or one the entry bound via\ninclude/linux/binfmt_misc.h:76: *         bpf_binprm_select_interp(), and return zero; a match is\ninclude/linux/binfmt_misc.h-77- *         committed, so a failure fails the exec instead of falling\n--\ntools/testing/selftests/exec/bpf_interp.bpf.c=14=char _license[] SEC(\"license\") = \"GPL\";\n--\ntools/testing/selftests/exec/bpf_interp.bpf.c-19-\ntools/testing/selftests/exec/bpf_interp.bpf.c:20:extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,\ntools/testing/selftests/exec/bpf_interp.bpf.c-21-\t\t\t\t size_t path__sz) __ksym;\n--\ntools/testing/selftests/exec/bpf_interp.bpf.c=43=int BPF_PROG(bpf_interp_load, struct linux_binprm *bprm)\n--\ntools/testing/selftests/exec/bpf_interp.bpf.c-45-\t/*\ntools/testing/selftests/exec/bpf_interp.bpf.c:46:\t * Keep the path on the (writable) stack: bpf_binprm_set_interp() takes\ntools/testing/selftests/exec/bpf_interp.bpf.c-47-\t * a sized memory arg and the verifier rejects a read-only .rodata\n--\ntools/testing/selftests/exec/bpf_interp.bpf.c-52-\t/* @path__sz includes the terminating NUL; 0 commits the selection. */\ntools/testing/selftests/exec/bpf_interp.bpf.c:53:\treturn bpf_binprm_set_interp(bprm, interp, sizeof(interp));\ntools/testing/selftests/exec/bpf_interp.bpf.c-54-}\n--\ntools/testing/selftests/exec/interp_bind.bpf.c=13=char _license[] SEC(\"license\") = \"GPL\";\n--\ntools/testing/selftests/exec/interp_bind.bpf.c-21-\ntools/testing/selftests/exec/interp_bind.bpf.c:22:extern int bpf_binprm_select_interp(struct linux_binprm *bprm,\ntools/testing/selftests/exec/interp_bind.bpf.c-23-\t\t\t\t    const char *name, size_t name__sz) __ksym;\n--\ntools/testing/selftests/exec/interp_bind.bpf.c=48=int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)\n--\ntools/testing/selftests/exec/interp_bind.bpf.c-61-\tcase EM_AARCH64:\ntools/testing/selftests/exec/interp_bind.bpf.c:62:\t\treturn bpf_binprm_select_interp(bprm, first, sizeof(first));\ntools/testing/selftests/exec/interp_bind.bpf.c-63-\tcase EM_RISCV:\ntools/testing/selftests/exec/interp_bind.bpf.c:64:\t\treturn bpf_binprm_select_interp(bprm, second, sizeof(second));\ntools/testing/selftests/exec/interp_bind.bpf.c-65-\t}\n--\ntools/testing/selftests/exec/interp_bind.bpf.c-67-\t/* The entry bound nothing under this name: -ENOENT fails the exec. */\ntools/testing/selftests/exec/interp_bind.bpf.c:68:\treturn bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));\ntools/testing/selftests/exec/interp_bind.bpf.c-69-}\n--\ntools/testing/selftests/exec/loader.bpf.c=12=char _license[] SEC(\"license\") = \"GPL\";\n--\ntools/testing/selftests/exec/loader.bpf.c-17-\ntools/testing/selftests/exec/loader.bpf.c:18:extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,\ntools/testing/selftests/exec/loader.bpf.c-19-\t\t\t\t size_t path__sz) __ksym;\n--\ntools/testing/selftests/exec/loader.bpf.c=38=int BPF_PROG(loader_load, struct linux_binprm *bprm)\n--\ntools/testing/selftests/exec/loader.bpf.c-47-\t/* @path__sz includes the terminating NUL; 0 commits the selection. */\ntools/testing/selftests/exec/loader.bpf.c:48:\treturn bpf_binprm_set_interp(bprm, interp, sizeof(interp));\ntools/testing/selftests/exec/loader.bpf.c-49-}\n--\ntools/testing/selftests/exec/nix_origin.bpf.c-7- * resolved relative to the directory of the binary being executed and\ntools/testing/selftests/exec/nix_origin.bpf.c:8: * selected via bpf_binprm_set_interp(). The match program reads the\ntools/testing/selftests/exec/nix_origin.bpf.c-9- * program headers itself, so anything else never commits to this\n--\ntools/testing/selftests/exec/nix_origin.bpf.c=38=extern int bpf_path_d_path(const struct path *path, char *buf,\ntools/testing/selftests/exec/nix_origin.bpf.c-39-\t\t\t   size_t buf__sz) __ksym;\ntools/testing/selftests/exec/nix_origin.bpf.c:40:extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,\ntools/testing/selftests/exec/nix_origin.bpf.c-41-\t\t\t\t size_t path__sz) __ksym;\n--\ntools/testing/selftests/exec/nix_origin.bpf.c=127=int BPF_PROG(nix_origin_load, struct linux_binprm *bprm)\n--\ntools/testing/selftests/exec/nix_origin.bpf.c-210-\ntools/testing/selftests/exec/nix_origin.bpf.c:211:\tret = bpf_binprm_set_interp(bprm, sc-\u003epath, rsz);\ntools/testing/selftests/exec/nix_origin.bpf.c-212-out_del:\n--\ntools/testing/selftests/exec/transparent.bpf.c=12=char _license[] SEC(\"license\") = \"GPL\";\n--\ntools/testing/selftests/exec/transparent.bpf.c-17-\ntools/testing/selftests/exec/transparent.bpf.c:18:extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,\ntools/testing/selftests/exec/transparent.bpf.c-19-\t\t\t\t size_t path__sz) __ksym;\n--\ntools/testing/selftests/exec/transparent.bpf.c=39=int BPF_PROG(transparent_load, struct linux_binprm *bprm)\n--\ntools/testing/selftests/exec/transparent.bpf.c-48-\t/* @path__sz includes the terminating NUL; 0 commits the selection. */\ntools/testing/selftests/exec/transparent.bpf.c:49:\treturn bpf_binprm_set_interp(bprm, interp, sizeof(interp));\ntools/testing/selftests/exec/transparent.bpf.c-50-}\n"
}

7/2 2026/08/01 02:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 33310
							output: 21
							thoughts: 349
						

Thoughts:
**My Initial Investigation into Interpreter Selection**

Okay, so I need to get a handle on how the kernel selects interpreters. My focus is on the functions directly involved in this process. Let's see, the ones that seem most relevant are: `binfmt_misc_find_interp`, `bpf_binprm_select_interp`, `bpf_binprm_set_interp`, `load_misc_binary`, `entry_select_interpreter`, and `entry_open_interpreter`.  I'll also keep `bm_entry_add_interp`, `bm_entry_write`, and `bm_register_write` in mind, even though some are static; listing them won't hurt, and might be helpful later.

Now, let me shift gears and dig into the code itself. I'll start with `fs/binfmt_misc.c` and hunt for the implementations of `bm_entry_write`, `bm_register_write`, and `bm_entry_add_interp`. Ah, right, those are file operations!  Okay, I can definitely list those.  Even though `bm_entry_write` is marked `static`, it's still a good place to poke around.

So, to summarize, the functions I need to look into are: `binfmt_misc_find_interp`, `bpf_binprm_select_interp`, `bpf_binprm_set_interp`, `load_misc_binary`, `bm_entry_write`, `bm_register_write`, `bm_entry_add_interp`, `entry_select_interpreter`, and `entry_open_interpreter`.

Next, I'll take a quick look in `Kconfig` to see how `BINFMT_MISC` and `BINFMT_MISC_BPF` are configured and potentially affect the selection process. This is the foundation I need to begin the deeper analysis.



8/2 2026/08/01 02:56 tool grepper
0m Args:
{
  "Expression": "config BINFMT_MISC"
}

Results:
{
  "Output": "fs/Kconfig.binfmt=138=config BINFMT_ZFLAT\n--\nfs/Kconfig.binfmt-144-\nfs/Kconfig.binfmt:145:config BINFMT_MISC\nfs/Kconfig.binfmt-146-\ttristate \"Kernel support for MISC binaries\"\n--\nfs/Kconfig.binfmt-170-\nfs/Kconfig.binfmt:171:config BINFMT_MISC_BPF\nfs/Kconfig.binfmt-172-\tbool \"BPF-selected interpreters for misc binaries\"\n"
}

9/2 2026/08/01 02:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 34260
							output: 35
							thoughts: 20
						

Thoughts:
Here's my attempt at summarizing the thought process, tailored for an expert audience:

**Diving into `fs/Kconfig.binfmt`**

Okay, let's get down to brass tacks. I'm going to take a look at the `fs/Kconfig.binfmt` file, specifically around line 171. My gut tells me there's something interesting or potentially relevant there in relation to what I'm working on, perhaps dealing with binary format configuration. Hopefully, it's something specific, maybe a conditional, a dependency definition, or a configuration option that's going to shed some light on the current problem. Time to see what's actually there.



10/2 2026/08/01 02:56 tool read-file
0m Args:
{
  "File": "fs/Kconfig.binfmt",
  "FirstLine": 160,
  "LineCount": 20
}

Results:
{
  "Contents": " 160:\t\t  feature, \u003cfile:Documentation/admin-guide/java.rst\u003e for information about how\n 161:\t\t  to include Java support. and \u003cfile:Documentation/admin-guide/mono.rst\u003e for\n 162:\t          information about how to include Mono-based .NET support.\n 163:\t\n 164:\t          To use binfmt_misc, you will need to mount it:\n 165:\t\t\tmount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc\n 166:\t\n 167:\t\t  You may say M here for module support and later load the module when\n 168:\t\t  you have use for it; the module is called binfmt_misc. If you\n 169:\t\t  don't know what to answer at this point, say Y.\n 170:\t\n 171:\tconfig BINFMT_MISC_BPF\n 172:\t\tbool \"BPF-selected interpreters for misc binaries\"\n 173:\t\tdepends on BINFMT_MISC=y\n 174:\t\tdepends on BPF_SYSCALL \u0026\u0026 BPF_JIT \u0026\u0026 DEBUG_INFO_BTF\n 175:\t\thelp\n 176:\t\t  Allow binfmt_misc binary type handlers to be implemented as bpf\n 177:\t\t  struct_ops programs. Instead of matching a fixed magic and\n 178:\t\t  redirecting to a fixed interpreter recorded at registration time\n 179:\t\t  such handlers match binaries programmatically and compute the\n"
}

11/2 2026/08/01 02:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 34861
							output: 190
							thoughts: 1151
						

Thoughts:
**Analysis of BINFMT_MISC and BPF Integration**

Okay, let's break this down. First, I need to ensure that `BINFMT_MISC_BPF` is enabled, which obviously relies on `BINFMT_MISC=y`, and then also the relevant BPF-specific configs: `BPF_SYSCALL`, `BPF_JIT`, and `DEBUG_INFO_BTF`. My immediate thought is to include both `BINFMT_MISC` and `BINFMT_MISC_BPF` in `EnableConfigs`. That covers the basic dependencies.

Now, where does this patch touch? Looks like `fs/binfmt_misc.c` and `fs/binfmt_misc_bpf.c`. So, `BINFMT_MISC` and `BINFMT_MISC_BPF` are the primary configurations in this area.

Next, I need to identify which functions are most relevant. My mental checklist focuses on these potential hotspots: `binfmt_misc_find_interp`, `bpf_binprm_select_interp`, `bpf_binprm_set_interp`, `load_misc_binary`, `bm_entry_write`, `bm_register_write`, `bm_entry_add_interp`, `entry_select_interpreter`, and `entry_open_interpreter`.

Now, let's refine this list. `load_misc_binary` is definitely a hot-path function, but it is specific to `binfmt_misc`. It's not a generic kernel function like `schedule` or `kmalloc`, so it's probably okay to include. `bm_entry_write` and `bm_register_write` are static, but they are crucial for setting up the functionality. The prompt says to favor non-static, non-inlined API entrypoint functions, but these are used as function pointers within `file_operations`, so they won't be inlined. They are good targets. `bm_entry_add_interp` appears static. Let's omit it as it's likely inlined by the compiler. Likewise, `entry_select_interpreter` and `entry_open_interpreter` are static, and probably inlined, so let's omit them too. `binfmt_misc_find_interp` is non-static. `bpf_binprm_select_interp` is non-static (a kfunc). `bpf_binprm_set_interp` is also non-static (a kfunc).

Putting all that together, I think the `FocusSymbols` should be:

- `binfmt_misc_find_interp`
- `bpf_binprm_select_interp`
- `bpf_binprm_set_interp`
- `load_misc_binary`
- `bm_entry_write`
- `bm_register_write`

Let's do a quick double-check on `load_misc_binary`... Yes, `static int load_misc_binary(struct linux_binprm *bprm)` in `fs/binfmt_misc.c`, and it's assigned to `misc_format.load_binary`, so we're good there.

Double-checking `bm_entry_write` and `bm_register_write`... Both are `static ssize_t` functions, and both are assigned to `file_operations.write`. Confirmed.

What about `binfmt_misc_find_interp`? Yes, it's non-static: `const struct binfmt_misc_interp * binfmt_misc_find_interp(const struct list_head *interps, const char *name)`.

`bpf_binprm_select_interp` is also non-static: `__bpf_kfunc int bpf_binprm_select_interp(struct linux_binprm *bprm, const char *name, size_t name__sz)`.

And `bpf_binprm_set_interp`? Non-static as well: `__bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, size_t path__sz)`.

So, the `Configs` I'm including are:

- `BINFMT_MISC`
- `BINFMT_MISC_BPF`

My reasoning is straightforward: this patch integrates BPF handlers into `binfmt_misc`, allowing interpreters to be bound and selected by name at `exec` time.


12/2 2026/08/01 02:56 tool set-results
0m Args:
{
  "EnableConfigs": [
    "BINFMT_MISC",
    "BINFMT_MISC_BPF"
  ],
  "FocusSymbols": [
    "binfmt_misc_find_interp",
    "bpf_binprm_select_interp",
    "bpf_binprm_set_interp",
    "load_misc_binary",
    "bm_entry_write",
    "bm_register_write"
  ],
  "Reasoning": "The patch introduces a new feature to binfmt_misc allowing BPF handlers to bind interpreters to an entry and select them by name at exec time. This modifies core kernel logic in fs/binfmt_misc.c and fs/binfmt_misc_bpf.c and is reachable from userspace via binfmt_misc configuration and execution of matching binaries.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "BINFMT_MISC",
    "BINFMT_MISC_BPF"
  ],
  "FocusSymbols": [
    "binfmt_misc_find_interp",
    "bpf_binprm_select_interp",
    "bpf_binprm_set_interp",
    "load_misc_binary",
    "bm_entry_write",
    "bm_register_write"
  ],
  "Reasoning": "The patch introduces a new feature to binfmt_misc allowing BPF handlers to bind interpreters to an entry and select them by name at exec time. This modifies core kernel logic in fs/binfmt_misc.c and fs/binfmt_misc_bpf.c and is reachable from userspace via binfmt_misc configuration and execution of matching binaries.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)