All of lore.kernel.org
 help / color / mirror / Atom feed
From: Farid Zakaria <farid.m.zakaria@gmail.com>
To: brauner@kernel.org
Cc: david.laight.linux@gmail.com, farid.m.zakaria@gmail.com,
	jack@suse.cz, jannh@google.com, kees@kernel.org,
	linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
	linux-kselftest@vger.kernel.org, linux-mm@kvack.org,
	mail@johnericson.me, shuah@kernel.org, viro@zeniv.linux.org.uk,
	Gemini <assistant@google.com>
Subject: [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection
Date: Sat,  4 Jul 2026 14:14:09 -0700	[thread overview]
Message-ID: <20260704211409.1978485-1-farid.m.zakaria@gmail.com> (raw)
In-Reply-To: <20260704-kochkunst-hauswand-blass-7226e9443a9f@brauner>

Historically binfmt_misc has only matched binaries by magic bytes or file
extension, and always redirects a match to a fixed interpreter recorded in
the registration. This is insufficient when the match requires parsing the
binary header (e.g. inspecting ELF program headers) or when the interpreter
must be computed per-binary rather than hard-coded.

Introduce a new 'B' (BPF) handler type. A pinned SOCKET_FILTER program is
registered in place of the magic/mask, and no interpreter is recorded:

  echo ':name:B:<bpf_pinned_path>::::<flags>' \
      > /proc/sys/fs/binfmt_misc/register

When a binary is executed, binfmt_misc runs the program with the
BINPRM_BUF_SIZE file-header buffer as context. Returning 1 selects the
handler; returning 0 falls through to the remaining handlers.

Unlike magic/extension handlers, a 'B' handler carries no interpreter of its
own: the program chooses it via a new helper, bpf_binprm_set_interp(). This
lets the program compute the interpreter path however it sees fit (for example
relative to the binary). A 'B' handler is therefore a strict superset of the
existing magic handlers -- any of them can be expressed as a program that
matches on the header and sets a fixed interpreter.

bpf_binprm_set_interp() is exposed to SOCKET_FILTER programs and stashes the
chosen path on a per-CPU area that binfmt_misc reads back immediately after
the run under migrate_disable(); only a match that set an interpreter
allocates.

Assisted-by: Gemini <assistant@google.com>
Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---

Hey Christian,

Thank you for the suggestion and discussion on the mailing list. I took a stab
at your idea of driving binfmt_misc interpreter selection from a BPF program
(hopefully this is what you had in mind). The other approach was also to use a
static interpreter during registration but this current approach feels right.

This prototype demonstrates binfmt_misc interpreter selection from a BPF program
so the interpreter can be *computed* (e.g. $ORIGIN-relative) rather than recorded
statically. It replaces my earlier "pluggable ELF interpreter loader registry" RFC.

As this is a first draft, a few things you probably will have notes on.
I tried my best but I am a novice here so I welcome your feedback.

 * The prototype uses socket filter type. This seemed the easiest to wire up to
   start. My guess is it should be a dedicated type (eg. BPF_PROG_TYPE_BINFMT).
 * bpf_binprm_set_interp() is a "classic helper", but that list
   is frozen in favour of kfuncs. A kfunc needs BTF and can't be called from
   the raw-bytecode selftest I wrote.
 * The program only gets bprm->buf today.
   That is enough to match on the ELF header but not to compute an
   $ORIGIN-relative path from the binary's location, which needs bprm->filename
 * I have some selftests that exercises match + program-chosen interpreter. I don't
   include them here yet since it seems unnecessary when discussing the idea.

To demo the functionality, I thought it would be neat to see how this is a superset
and can replace all the qemu binfmt usage [1].

  $ ./hello-aarch64;
  -bash: /etc/binfmt-demo/hello-aarch64: cannot execute binary file: Exec format error

  $ bpftool prog load filter.bpf.o /sys/fs/bpf/qemu type socket
  $ echo ':qemu-bpf:B:/sys/fs/bpf/qemu::::P' > /proc/sys/fs/binfmt_misc/register

  $ ./hello-aarch64
  AARCH64_RAN_VIA_QEMU

All the per-CPU guards were recommended by AI.

[1] https://gist.github.com/fzakaria/bef27d2e21b0e36ffccda1cbf417b636

 fs/binfmt_misc.c         | 181 ++++++++++++++++++++++++++++++++++++---
 include/linux/bpf.h      |   1 +
 include/uapi/linux/bpf.h |   1 +
 net/core/filter.c        |   8 ++
 4 files changed, 180 insertions(+), 11 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 84349fcb9..cf6698d59 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -29,6 +29,11 @@
 #include <linux/fs.h>
 #include <linux/uaccess.h>
 
+#ifdef CONFIG_BPF_SYSCALL
+#include <linux/bpf.h>
+#include <linux/filter.h>
+#endif
+
 #include "internal.h"
 
 #ifdef DEBUG
@@ -41,12 +46,14 @@ 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)
 #define MISC_FMT_OPEN_FILE (1UL << 28)
 
+struct bpf_prog;
+
 typedef struct {
 	struct list_head list;
 	unsigned long flags;		/* type, status, etc. */
@@ -59,6 +66,9 @@ typedef struct {
 	struct dentry *dentry;
 	struct file *interp_file;
 	refcount_t users;		/* sync removal with load_misc_binary() */
+#ifdef CONFIG_BPF_SYSCALL
+	struct bpf_prog *bpf_prog;
+#endif
 } Node;
 
 static struct file_system_type bm_fs_type;
@@ -78,10 +88,51 @@ static struct file_system_type bm_fs_type;
  */
 #define MAX_REGISTER_LENGTH 1920
 
+#ifdef CONFIG_BPF_SYSCALL
+struct binfmt_bpf_interp {
+	char path[PATH_MAX];
+	int len;	/* < 0 if the current program set no interpreter */
+};
+static DEFINE_PER_CPU(struct binfmt_bpf_interp, binfmt_bpf_interp);
+
+/*
+ * bpf_binprm_set_interp - let a binfmt_misc 'B' program pick the interpreter.
+ * @path: interpreter path, in BPF-accessible memory
+ * @len:  number of bytes in @path
+ *
+ * The program computes the interpreter path however it sees fit (e.g. relative
+ * to the binary). The path is stashed on a per-CPU area that binfmt_misc reads
+ * back immediately after running the program under migrate_disable(), so it
+ * cannot race with another CPU.
+ */
+BPF_CALL_2(bpf_binprm_set_interp, const char *, path, u32, len)
+{
+	struct binfmt_bpf_interp *sc = this_cpu_ptr(&binfmt_bpf_interp);
+
+	if (len == 0 || len >= PATH_MAX)
+		return -EINVAL;
+	/* @path is @len bytes of BPF memory and is not NUL-terminated. */
+	memcpy(sc->path, path, len);
+	sc->path[len] = '\0';
+	sc->len = len;
+	return 0;
+}
+
+const struct bpf_func_proto bpf_binprm_set_interp_proto = {
+	.func		= bpf_binprm_set_interp,
+	.gpl_only	= false,
+	.ret_type	= RET_INTEGER,
+	.arg1_type	= ARG_PTR_TO_MEM | MEM_RDONLY,
+	.arg2_type	= ARG_CONST_SIZE,
+};
+#endif /* CONFIG_BPF_SYSCALL */
+
 /**
  * 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_interp: receives a kmalloc'd interpreter path if a 'B' program chooses
+ *              one via bpf_binprm_set_interp(); the caller must kfree() it
  *
  * Search for a binary type handler for @bprm in the list of registered binary
  * type handlers.
@@ -89,7 +140,8 @@ static struct file_system_type bm_fs_type;
  * 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,
+				   char **bpf_interp)
 {
 	char *p = strrchr(bprm->interp, '.');
 	Node *e;
@@ -103,6 +155,37 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc,
 		if (!test_bit(Enabled, &e->flags))
 			continue;
 
+		/* Do matching based on BPF if applicable. */
+		if (test_bit(Bpf, &e->flags)) {
+#ifdef CONFIG_BPF_SYSCALL
+			if (e->bpf_prog) {
+				struct binfmt_bpf_interp *sc;
+				u32 ret;
+
+				migrate_disable();
+				sc = this_cpu_ptr(&binfmt_bpf_interp);
+				sc->len = -1;
+
+				rcu_read_lock();
+				ret = bpf_prog_run(e->bpf_prog, bprm->buf);
+				rcu_read_unlock();
+
+				if (ret == 1 && sc->len > 0)
+					*bpf_interp = kmemdup_nul(sc->path,
+								  sc->len,
+								  GFP_ATOMIC);
+				migrate_enable();
+
+				pr_debug("binfmt_misc: ran BPF program for %s, ret = %u\n",
+					 bprm->filename, ret);
+
+				if (ret == 1)
+					return e;
+			}
+#endif
+			continue;
+		}
+
 		/* Do matching based on extension if applicable. */
 		if (!test_bit(Magic, &e->flags)) {
 			if (p && !strcmp(e->magic, p + 1))
@@ -139,12 +222,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,
+				char **bpf_interp)
 {
 	Node *e;
 
 	read_lock(&misc->entries_lock);
-	e = search_binfmt_handler(misc, bprm);
+	e = search_binfmt_handler(misc, bprm, bpf_interp);
 	if (e)
 		refcount_inc(&e->users);
 	read_unlock(&misc->entries_lock);
@@ -164,6 +248,10 @@ 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);
+#ifdef CONFIG_BPF_SYSCALL
+		if (test_bit(Bpf, &e->flags) && e->bpf_prog)
+			bpf_prog_put(e->bpf_prog);
+#endif
 		kfree(e);
 	}
 }
@@ -206,15 +294,27 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	struct file *interp_file = NULL;
 	int retval = -ENOEXEC;
 	struct binfmt_misc *misc;
+	char *bpf_interp __free(kfree) = NULL;
+	const char *interpreter;
 
 	misc = load_binfmt_misc();
 	if (!misc->enabled)
 		return retval;
 
-	fmt = get_binfmt_handler(misc, bprm);
+	fmt = get_binfmt_handler(misc, bprm, &bpf_interp);
 	if (!fmt)
 		return retval;
 
+	/*
+	 * A 'B' (BPF) handler carries no interpreter of its own; the program
+	 * chooses it via bpf_binprm_set_interp(). Other handlers use the
+	 * interpreter recorded at registration.
+	 */
+	interpreter = bpf_interp ? bpf_interp : fmt->interpreter;
+	retval = -ENOEXEC;
+	if (!interpreter[0])
+		goto ret;
+
 	/* Need to be able to load the file after exec */
 	retval = -ENOENT;
 	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
@@ -238,22 +338,27 @@ 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;
 
-	if (fmt->flags & MISC_FMT_OPEN_FILE) {
+	/*
+	 * The pre-opened interp_file (MISC_FMT_OPEN_FILE / 'F' flag) only
+	 * applies to the statically registered interpreter; a program-supplied
+	 * path is opened here.
+	 */
+	if ((fmt->flags & MISC_FMT_OPEN_FILE) && !bpf_interp) {
 		interp_file = file_clone_open(fmt->interp_file);
 		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))
@@ -404,6 +509,10 @@ 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':
+		pr_debug("register: type: B (bpf)\n");
+		e->flags = (1 << Enabled) | (1 << Bpf);
+		break;
 	default:
 		goto einval;
 	}
@@ -492,6 +601,45 @@ static Node *create_entry(const char __user *buffer, size_t count)
 				}
 			}
 		}
+	} else if (test_bit(Bpf, &e->flags)) {
+		/* Handle the 'B' (BPF) format. */
+		char *s;
+
+		/* The offset field actually holds the pinned BPF program path */
+		s = strchr(p, del);
+		if (!s)
+			goto einval;
+		*s++ = '\0';
+		e->magic = p; /* Keep path in e->magic */
+		pr_debug("register: bpf program path: %s\n", e->magic);
+
+#ifdef CONFIG_BPF_SYSCALL
+		e->bpf_prog = bpf_prog_get_type_path(e->magic, BPF_PROG_TYPE_SOCKET_FILTER);
+		if (IS_ERR(e->bpf_prog)) {
+			err = PTR_ERR(e->bpf_prog);
+			e->bpf_prog = NULL;
+			kfree(e);
+			return ERR_PTR(err);
+		}
+#else
+		goto einval;
+#endif
+
+		p = s;
+
+		/* The magic field is unused, must be empty */
+		s = strchr(p, del);
+		if (!s || p != s)
+			goto einval;
+		*s++ = '\0';
+		p = s;
+
+		/* The mask field is unused, must be empty */
+		s = strchr(p, del);
+		if (!s || p != s)
+			goto einval;
+		*s++ = '\0';
+		p = s;
 	} else {
 		/* Handle the 'E' (extension) format. */
 
@@ -524,8 +672,17 @@ 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)) {
+		/*
+		 * A 'B' (BPF) handler carries no interpreter of its own; the
+		 * program picks it via bpf_binprm_set_interp(). Reject a
+		 * statically registered one.
+		 */
+		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. */
@@ -602,7 +759,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)) {
+		sprintf(dp, "bpf %s\n", e->magic);
+	} else if (!test_bit(Magic, &e->flags)) {
 		sprintf(dp, "extension .%s\n", e->magic);
 	} else {
 		dp += sprintf(dp, "offset %i\nmagic ", e->offset);
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 7719f6528..3aef44b69 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3874,6 +3874,7 @@ extern const struct bpf_func_proto bpf_ktime_get_ns_proto;
 extern const struct bpf_func_proto bpf_ktime_get_boot_ns_proto;
 extern const struct bpf_func_proto bpf_ktime_get_tai_ns_proto;
 extern const struct bpf_func_proto bpf_get_current_pid_tgid_proto;
+extern const struct bpf_func_proto bpf_binprm_set_interp_proto;
 extern const struct bpf_func_proto bpf_get_current_uid_gid_proto;
 extern const struct bpf_func_proto bpf_get_current_comm_proto;
 extern const struct bpf_func_proto bpf_get_stackid_proto;
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5f..dce155c5b 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -6142,6 +6142,7 @@ union bpf_attr {
 	FN(user_ringbuf_drain, 209, ##ctx)		\
 	FN(cgrp_storage_get, 210, ##ctx)		\
 	FN(cgrp_storage_delete, 211, ##ctx)		\
+	FN(binprm_set_interp, 212, ##ctx)		\
 	/* This helper list is effectively frozen. If you are trying to	\
 	 * add a new helper, you should add a kfunc instead which has	\
 	 * less stability guarantees. See Documentation/bpf/kfuncs.rst	\
diff --git a/net/core/filter.c b/net/core/filter.c
index 2e96b4b84..187692a4a 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -8397,6 +8397,14 @@ sk_filter_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
 		return &bpf_get_socket_uid_proto;
 	case BPF_FUNC_perf_event_output:
 		return &bpf_skb_event_output_proto;
+#if IS_BUILTIN(CONFIG_BINFMT_MISC)
+	case BPF_FUNC_binprm_set_interp:
+		/*
+		 * binfmt_misc reuses SOCKET_FILTER programs to select an
+		 * interpreter; expose the helper that lets them set it.
+		 */
+		return &bpf_binprm_set_interp_proto;
+#endif
 	default:
 		return bpf_sk_base_func_proto(func_id, prog);
 	}
-- 
2.51.2


  reply	other threads:[~2026-07-04 21:14 UTC|newest]

Thread overview: 36+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-22  4:39 [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Farid Zakaria
2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
2026-06-22  9:53   ` Jori Koolstra
2026-06-23 20:14   ` Kees Cook
2026-06-23 20:35     ` Farid Zakaria
2026-06-22  4:39 ` [PATCH 2/2] selftests/exec: add test suites for $ORIGIN interpreter resolution Farid Zakaria
2026-06-22 10:39 ` [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Jan Kara
2026-06-22 17:15   ` Farid Zakaria
2026-06-22 21:08     ` John Ericson
2026-06-25  8:50       ` Christian Brauner
2026-06-25 19:34         ` John Ericson
2026-06-26 12:39         ` Jann Horn
2026-06-26 13:26           ` David Laight
2026-06-26 13:34             ` Jann Horn
2026-06-26 13:40               ` Farid Zakaria
2026-06-26 16:28               ` David Laight
2026-06-28 12:36           ` Christian Brauner
2026-06-28 13:20             ` Christian Brauner
2026-06-28 18:44               ` David Laight
2026-06-30 16:58               ` Farid Zakaria
2026-07-02  6:47                 ` Christian Brauner
2026-06-30 17:59               ` David Laight
2026-07-02 21:42                 ` [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry Farid Zakaria
2026-07-03  9:22                   ` Christian Brauner
2026-07-03 18:32                     ` Farid Zakaria
2026-07-04  8:15                       ` Christian Brauner
2026-07-04 21:14                         ` Farid Zakaria [this message]
2026-07-06 16:01                           ` [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection Christian Brauner
2026-07-06 16:47                             ` Farid Zakaria
2026-07-07 11:34                               ` Christian Brauner
2026-07-07 19:45                                 ` Christian Brauner
2026-07-07 22:44                                   ` Farid Zakaria
2026-07-08  9:14                                     ` Christian Brauner
2026-07-08 16:47                                       ` Christian Brauner
2026-07-09  4:07                                         ` Farid Zakaria
2026-07-09  8:46                                           ` Christian Brauner

Reply instructions:

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

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

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

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

  git send-email \
    --in-reply-to=20260704211409.1978485-1-farid.m.zakaria@gmail.com \
    --to=farid.m.zakaria@gmail.com \
    --cc=assistant@google.com \
    --cc=brauner@kernel.org \
    --cc=david.laight.linux@gmail.com \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=kees@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=mail@johnericson.me \
    --cc=shuah@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

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

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