From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0EB353AB5C7; Tue, 7 Jul 2026 19:37:17 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783453038; cv=none; b=eccioea8BQ1gVBcY8jZgGYqPfyznz7LDH/lBJCpPu8ATS1bGLcJpWhg8aXeteme1RQLM9QhwXqKuUNymVOLkd03Ou9DBHg3LmU6vV5hau88VPLiNKGz0XtxSwM1leuLi53Ax5nQmX5Pro2iKFR5BDuui0QxA8GRnS38LuZ3BipE= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783453038; c=relaxed/simple; bh=c2bxGFoxQxDhi2AAos8sEU+11LFeMkVJChF5CpobtPQ=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=aS1ur/TAAAubeHzZxMk/XYh2aBS3cTAC9Fk1DI3jbv570ph9R9bHUSZUFKRnbu+ponCL6NhTlDhrMd/1mObj7/ZbW5Cxp2R8B49d7PQWcvULCPUo49rhNH56ytnqwiiTmUA/fEg9cFvYM1hMvA69W23GYyph72Q7WVfPb3mzGZQ= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=NgX2XPMd; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="NgX2XPMd" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 2604B1F00A3A; Tue, 7 Jul 2026 19:37:13 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1783453036; bh=7H+nuLsu/xLrsPGwGk2H34dZOFArN4uMoNlNdacm5v8=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=NgX2XPMdQkoWHrASfo053PJ9DQD2Lx4hfIYNacOsyE+8QheFrJrZrS0xitm7c6bYx fq/HF1WVMEVWIfZLanNQWuBqeOvyY9yPP2qcdmxPKjfkGLROE1YRzezAvLJiknxuBz ChGxm7SFRVZv9qyg0/EIrgsuzmwzPN0o9qRh2sL29n3loFffUYqpTwY3pmmqc2mU75 3ae8iaMLjjh2q9Uv28pELa0lpNokXSefnJ1pnQDGeRqNHLprnXIG+yTOp9ZdgGMlX8 dtLEEnQYIHMI+9ef8eUVQBn2kFb9ATx53MLCD1UXIQy8zH4YRK/lTZ7dCVVUu5WpOY rG+O/WmVWTvEA== From: Christian Brauner Date: Tue, 07 Jul 2026 21:36:47 +0200 Subject: [PATCH RFC POC 3/4] binfmt_misc: wire up bpf-backed 'B' entries Precedence: bulk X-Mailing-List: linux-fsdevel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Message-Id: <20260707-work-bpf-binfmt_misc-v1-3-74b995c84ec1@kernel.org> References: <20260707-work-bpf-binfmt_misc-v1-0-74b995c84ec1@kernel.org> In-Reply-To: <20260707-work-bpf-binfmt_misc-v1-0-74b995c84ec1@kernel.org> To: Farid Zakaria Cc: Daniel Borkmann , Alexei Starovoitov , Kees Cook , Alexander Viro , Jan Kara , Jonathan Corbet , linux-fsdevel@vger.kernel.org, linux-mm@kvack.org, linux-kernel@vger.kernel.org, bpf@vger.kernel.org, jannh@google.com, linux-mm@kvack.org, mail@johnericson.me, "Christian Brauner (Amutable)" X-Mailer: b4 0.16-dev-4217c X-Developer-Signature: v=1; a=openpgp-sha256; l=15992; i=brauner@kernel.org; h=from:subject:message-id; bh=c2bxGFoxQxDhi2AAos8sEU+11LFeMkVJChF5CpobtPQ=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWT5hib4XYszVViuuXGC39NdOg5SYoWL79n/CpiRV7fJS 3Lj+vQbHaUsDGJcDLJiiiwO7Sbhcst5KjYbZWrAzGFlAhnCwMUpABPJmsPIsPOO++WM6qUveNRl XyRLJzruyWI8djko65F/Atf5QkH/ZYwMy25v1H23O3HpvB8h11tniqckiJglubCW/GvV41qbG8j KAQA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Activate a registered binfmt_misc_ops handler through the existing text interface with the new 'B' entry type: echo ':name:B:::::' > /register The offset field carries the handler name; magic, mask, and interpreter must be empty since the program supplies both the matching and the interpreter. Reusing the register file keeps the existing permission model intact: activating a handler requires the same write access to a binfmt_misc instance as any other registration, and the per user namespace instance semantics apply unchanged. A 'B' entry in a container's own instance shadows the host's handlers just like any other entry, and the privilege needed to shadow e.g. all ELF binaries is the same as for a static 'M' entry matching \x7fELF today; the only novelty is that matching becomes programmable. The entry takes its own reference on the ops for its whole lifetime. It is dropped in put_binfmt_handler() next to the MISC_FMT_OPEN_FILE interp_file cleanup, which the existing users refcount already defers past any concurrent load_misc_binary(), and explicitly on the registration failure path where the users refcount is not live yet. The program runs from load_misc_binary(), never from the matching walk under entries_lock: the read_lock disables preemption while the load program must be able to sleep to read file content. search_binfmt_handler() therefore only nominates the next enabled 'B' entry and the program decides outside the lock. A declining program (returning 0) falls through to handlers registered after it via a skip cursor and a rescan; entries registered or removed between rescans can shift the cursor, so a program may be consulted twice in that window, which is harmless since matching must be free of side effects. Returning a positive value without having selected an interpreter terminates the binfmt_misc scan with -ENOEXEC so the remaining binary formats still get a shot. The 'C' and 'F' flags are rejected for 'B' entries. 'F' exists to pre-open a fixed interpreter at registration time in the registrar's context which is meaningless for a per-exec computed path. 'C' honors the suid bits of the matched binary while executing the interpreter; combined with a program-chosen interpreter that would let a user namespace root pick what runs with a setuid binary's credentials. The computed path itself cannot widen access: it is opened with open_exec() under the caller's credentials with the usual LSM and noexec checks, identical to a statically registered interpreter. Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 40 ++++++++- fs/binfmt_misc.c | 143 +++++++++++++++++++++++++++--- 2 files changed, 168 insertions(+), 15 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index c0a34fbf8022..c0efb0773628 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -26,11 +26,13 @@ Here is what the fields mean: name below ``/proc/sys/fs/binfmt_misc``; cannot contain slashes ``/`` for obvious reasons. - ``type`` - is the type of recognition. Give ``M`` for magic and ``E`` for extension. + is the type of recognition. Give ``M`` for magic, ``E`` for extension and + ``B`` for a bpf-backed handler (see below). - ``offset`` is the offset of the magic/mask in the file, counted in bytes. This defaults to 0 if you omit it (i.e. you write ``:name:type::magic...``). - Ignored when using filename extension matching. + Ignored when using filename extension matching. For ``B`` entries this + field carries the name of the bpf handler instead. - ``magic`` is the byte sequence binfmt_misc is matching for. The magic string may contain hex-encoded characters like ``\x0a`` or ``\xA4``. Note that you @@ -97,6 +99,40 @@ There are some restrictions: offset+size(magic) has to be less than 128 - the interpreter string may not exceed 127 characters + +bpf-backed handlers +------------------- + +With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter +selection can be delegated to a bpf program. A handler is an instance of the +``binfmt_misc_ops`` struct_ops with a sleepable ``load`` program and a +``name``. Once the struct_ops map is registered the handler can be activated +with a ``B`` entry that references it by name and carries neither magic, +mask, nor interpreter:: + + echo ':qemu:B:my_handler::::' > register + +At exec time the ``load`` program receives the ``linux_binprm`` of the +binary. It can match on the header in ``bprm->buf``, read the file itself, +e.g. to parse ELF program headers, and derive the interpreter from the +binary's location. It selects the interpreter by calling the +``bpf_binprm_set_interp()`` kfunc with an absolute path and returning a +positive value. Returning ``0`` falls through to the handlers registered +after this one, a negative errno fails the exec with that error; +``-ENOEXEC`` ends the binfmt_misc search but lets the remaining binary +formats have a go. The interpreter is opened with the credentials of the +task doing the exec, exactly as a statically registered interpreter would +be. + +Handlers are looked up in the user namespace the struct_ops map was +registered in, falling back to ancestor namespaces, mirroring how +binfmt_misc instances themselves are looked up. The entry keeps the handler +alive; deleting the struct_ops map only prevents new registrations. + +The ``C`` and ``F`` flags cannot be combined with ``B`` entries: there is no +fixed interpreter to pre-open and a program-selected interpreter must never +inherit the credentials of a setuid binary. + To use binfmt_misc you have to mount it first. You can mount it with ``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 84349fcb93f1..b80e219ba587 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -41,7 +42,7 @@ enum { VERBOSE_STATUS = 1 /* make it zero to save 400 bytes kernel memory */ }; -enum {Enabled, Magic}; +enum {Enabled, Magic, Bpf}; #define MISC_FMT_PRESERVE_ARGV0 (1UL << 31) #define MISC_FMT_OPEN_BINARY (1UL << 30) #define MISC_FMT_CREDENTIALS (1UL << 29) @@ -58,6 +59,8 @@ typedef struct { char *name; struct dentry *dentry; struct file *interp_file; + const struct binfmt_misc_ops *bpf_ops; /* bpf-backed handler ('B') */ + const char *bpf_ops_name; refcount_t users; /* sync removal with load_misc_binary() */ } Node; @@ -82,14 +85,19 @@ static struct file_system_type bm_fs_type; * search_binfmt_handler - search for a binary handler for @bprm * @misc: handle to binfmt_misc instance * @bprm: binary for which we are looking for a handler + * @bpf_skip: number of bpf-backed handlers to skip over * * Search for a binary type handler for @bprm in the list of registered binary - * type handlers. + * type handlers. A bpf-backed handler cannot be matched here as its program + * must run in sleepable context; it is returned as a candidate and the + * program decides in load_misc_binary(). @bpf_skip allows the caller to + * resume the search after the first @bpf_skip candidates declined. * * Return: binary type list entry on success, NULL on failure */ static Node *search_binfmt_handler(struct binfmt_misc *misc, - struct linux_binprm *bprm) + struct linux_binprm *bprm, + unsigned int bpf_skip) { char *p = strrchr(bprm->interp, '.'); Node *e; @@ -103,6 +111,15 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, if (!test_bit(Enabled, &e->flags)) continue; + /* The program decides in load_misc_binary(). */ + if (test_bit(Bpf, &e->flags)) { + if (bpf_skip) { + bpf_skip--; + continue; + } + return e; + } + /* Do matching based on extension if applicable. */ if (!test_bit(Magic, &e->flags)) { if (p && !strcmp(e->magic, p + 1)) @@ -132,6 +149,7 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, * get_binfmt_handler - try to find a binary type handler * @misc: handle to binfmt_misc instance * @bprm: binary for which we are looking for a handler + * @bpf_skip: number of bpf-backed handlers to skip over * * Try to find a binfmt handler for the binary type. If one is found take a * reference to protect against removal via bm_{entry,status}_write(). @@ -139,12 +157,13 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, * Return: binary type list entry on success, NULL on failure */ static Node *get_binfmt_handler(struct binfmt_misc *misc, - struct linux_binprm *bprm) + struct linux_binprm *bprm, + unsigned int bpf_skip) { Node *e; read_lock(&misc->entries_lock); - e = search_binfmt_handler(misc, bprm); + e = search_binfmt_handler(misc, bprm, bpf_skip); if (e) refcount_inc(&e->users); read_unlock(&misc->entries_lock); @@ -164,6 +183,8 @@ static void put_binfmt_handler(Node *e) if (refcount_dec_and_test(&e->users)) { if (e->flags & MISC_FMT_OPEN_FILE) filp_close(e->interp_file, NULL); + if (e->bpf_ops) + binfmt_misc_put_ops(e->bpf_ops); kfree(e); } } @@ -206,12 +227,15 @@ static int load_misc_binary(struct linux_binprm *bprm) struct file *interp_file = NULL; int retval = -ENOEXEC; struct binfmt_misc *misc; + const char *interpreter; + unsigned int bpf_skip = 0; misc = load_binfmt_misc(); if (!misc->enabled) return retval; - fmt = get_binfmt_handler(misc, bprm); +retry: + fmt = get_binfmt_handler(misc, bprm, bpf_skip); if (!fmt) return retval; @@ -220,6 +244,32 @@ static int load_misc_binary(struct linux_binprm *bprm) if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) goto ret; + if (test_bit(Bpf, &fmt->flags)) { + retval = fmt->bpf_ops->load(bprm); + if (retval < 0) { + /* Keep a program-supplied error within errno range. */ + if (retval < -MAX_ERRNO) + retval = -ENOEXEC; + goto ret; + } + if (!retval) { + /* Declined, move on to later handlers. */ + kfree(bprm->bpf_interp); + bprm->bpf_interp = NULL; + put_binfmt_handler(fmt); + bpf_skip++; + retval = -ENOEXEC; + goto retry; + } + /* Selecting an interpreter is part of the contract. */ + retval = -ENOEXEC; + if (!bprm->bpf_interp) + goto ret; + interpreter = bprm->bpf_interp; + } else { + interpreter = fmt->interpreter; + } + if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) { bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0; } else { @@ -238,13 +288,13 @@ static int load_misc_binary(struct linux_binprm *bprm) bprm->argc++; /* add the interp as argv[0] */ - retval = copy_string_kernel(fmt->interpreter, bprm); + retval = copy_string_kernel(interpreter, bprm); if (retval < 0) goto ret; bprm->argc++; /* Update interp in case binfmt_script needs it. */ - retval = bprm_change_interp(fmt->interpreter, bprm); + retval = bprm_change_interp(interpreter, bprm); if (retval < 0) goto ret; @@ -253,7 +303,7 @@ static int load_misc_binary(struct linux_binprm *bprm) if (!IS_ERR(interp_file)) deny_write_access(interp_file); } else { - interp_file = open_exec(fmt->interpreter); + interp_file = open_exec(interpreter); } retval = PTR_ERR(interp_file); if (IS_ERR(interp_file)) @@ -265,6 +315,9 @@ static int load_misc_binary(struct linux_binprm *bprm) retval = 0; ret: + /* A program-selected interpreter is consumed by this exec attempt. */ + kfree(bprm->bpf_interp); + bprm->bpf_interp = NULL; /* * If we actually put the node here all concurrent calls to @@ -404,13 +457,47 @@ static Node *create_entry(const char __user *buffer, size_t count) pr_debug("register: type: M (magic)\n"); e->flags = (1 << Enabled) | (1 << Magic); break; + case 'B': + if (!IS_ENABLED(CONFIG_BINFMT_MISC_BPF)) + goto einval; + pr_debug("register: type: B (bpf)\n"); + e->flags = (1 << Enabled) | (1 << Bpf); + break; default: goto einval; } if (*p++ != del) goto einval; - if (test_bit(Magic, &e->flags)) { + if (test_bit(Bpf, &e->flags)) { + char *s; + + /* The 'offset' field carries the handler name. */ + s = strchr(p, del); + if (!s) + goto einval; + *s++ = '\0'; + e->bpf_ops_name = p; + if (!e->bpf_ops_name[0] || + strlen(e->bpf_ops_name) >= BINFMT_MISC_OPS_NAME_MAX) + goto einval; + p = s; + pr_debug("register: bpf handler: {%s}\n", e->bpf_ops_name); + + /* The 'magic' field must be empty. */ + s = strchr(p, del); + if (!s || s != p) + goto einval; + *s++ = '\0'; + p = s; + + /* The 'mask' field must be empty. */ + s = strchr(p, del); + if (!s || s != p) + goto einval; + *s++ = '\0'; + p = s; + } else if (test_bit(Magic, &e->flags)) { /* Handle the 'M' (magic) format. */ char *s; @@ -524,8 +611,13 @@ static Node *create_entry(const char __user *buffer, size_t count) if (!p) goto einval; *p++ = '\0'; - if (!e->interpreter[0]) + if (test_bit(Bpf, &e->flags)) { + /* The program selects the interpreter at exec time. */ + if (e->interpreter[0]) + goto einval; + } else if (!e->interpreter[0]) { goto einval; + } pr_debug("register: interpreter: {%s}\n", e->interpreter); /* Parse the 'flags' field. */ @@ -535,6 +627,14 @@ static Node *create_entry(const char __user *buffer, size_t count) if (p != buf + count) goto einval; + /* + * A program-selected interpreter cannot be pre-opened and must not + * inherit the credentials of a setuid binary it was chosen for. + */ + if (test_bit(Bpf, &e->flags) && + (e->flags & (MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE))) + goto einval; + return e; out: @@ -588,7 +688,10 @@ static void entry_status(Node *e, char *page) return; } - dp += sprintf(dp, "%s\ninterpreter %s\n", status, e->interpreter); + if (test_bit(Bpf, &e->flags)) + dp += sprintf(dp, "%s\nbpf %s\n", status, e->bpf_ops_name); + else + dp += sprintf(dp, "%s\ninterpreter %s\n", status, e->interpreter); /* print the special flags */ dp += sprintf(dp, "flags: "); @@ -602,7 +705,9 @@ static void entry_status(Node *e, char *page) *dp++ = 'F'; *dp++ = '\n'; - if (!test_bit(Magic, &e->flags)) { + if (test_bit(Bpf, &e->flags)) { + *dp = '\0'; + } else if (!test_bit(Magic, &e->flags)) { sprintf(dp, "extension .%s\n", e->magic); } else { dp += sprintf(dp, "offset %i\nmagic ", e->offset); @@ -809,6 +914,16 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, if (IS_ERR(e)) return PTR_ERR(e); + if (test_bit(Bpf, &e->flags)) { + e->bpf_ops = binfmt_misc_get_ops(sb->s_user_ns, e->bpf_ops_name); + if (!e->bpf_ops) { + pr_notice("register: no bpf handler named %s\n", + e->bpf_ops_name); + kfree(e); + return -ENOENT; + } + } + if (e->flags & MISC_FMT_OPEN_FILE) { /* * Now that we support unprivileged binfmt_misc mounts make @@ -834,6 +949,8 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, exe_file_allow_write_access(f); filp_close(f, NULL); } + if (e->bpf_ops) + binfmt_misc_put_ops(e->bpf_ops); kfree(e); return err; } -- 2.53.0