Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: Farid Zakaria <farid.m.zakaria@gmail.com>
Cc: Daniel Borkmann <daniel@iogearbox.net>,
	 Alexei Starovoitov <ast@kernel.org>, Kees Cook <kees@kernel.org>,
	 Alexander Viro <viro@zeniv.linux.org.uk>,
	Jan Kara <jack@suse.cz>,  Jonathan Corbet <corbet@lwn.net>,
	linux-fsdevel@vger.kernel.org,  linux-mm@kvack.org,
	bpf@vger.kernel.org, jannh@google.com,  mail@johnericson.me,
	"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH v2 8/9] binfmt_misc: let a bpf handler run the interpreter transparently
Date: Tue, 14 Jul 2026 21:58:13 +0200	[thread overview]
Message-ID: <20260714-work-bpf-binfmt_misc-v2-8-57b7529c002c@kernel.org> (raw)
In-Reply-To: <20260714-work-bpf-binfmt_misc-v2-0-57b7529c002c@kernel.org>

A binfmt_misc interpreter is visible to the binary it runs: argv[0]
becomes the interpreter path, the binary's path is appended as an
argument and /proc/pid/cmdline shows both. For wine or qemu-user that is
the point, but for a per-binary loader the interpreter is an
implementation detail of running the binary that has no business in the
argument vector. And a binary handed to execveat() as an O_CLOEXEC fd
without a usable path cannot be run through binfmt_misc at all: the
interpreter would have no path to open the binary by, so
load_misc_binary() refuses upfront with -ENOENT.

Add BPF_BINPRM_TRANSPARENT. It goes beyond the static flags: the binary
is handed to the interpreter through AT_EXECFD as with BPF_BINPRM_EXECFD,
but the argument vector and bprm->interp are also left exactly as the
caller set them, so argv[0] and /proc/pid/cmdline look like a direct
execution of the binary. The interpreter has to load the binary from
AT_EXECFD for this; a relocatable loader can, glibc's ld.so does not.

The inaccessible-path bail moves after the load program has run and into
the path-building branch: a transparent interpreter takes the binary from
AT_EXECFD instead of a path, so the restriction no longer applies and the
O_CLOEXEC execveat() case above just works. BPF_BINPRM_PRESERVE_ARGV0 is
subsumed - transparency preserves the whole argument vector, argv[0]
included - and a staged interpreter argument is dropped: no argv slot is
built for it to land in.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/admin-guide/binfmt-misc.rst |  9 +++
 fs/binfmt_misc.c                          | 92 ++++++++++++++++++-------------
 fs/binfmt_misc_bpf.c                      |  6 +-
 include/linux/binfmt_misc.h               |  3 +
 4 files changed, 71 insertions(+), 39 deletions(-)

diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst
index 7f42abf9cfba..b446b3be9628 100644
--- a/Documentation/admin-guide/binfmt-misc.rst
+++ b/Documentation/admin-guide/binfmt-misc.rst
@@ -148,6 +148,15 @@ decide them differently for each binary it handles:
 - ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and
   passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so
   the interpreter can run binaries it could not open by path.
+- ``BPF_BINPRM_TRANSPARENT`` runs the interpreter transparently. It has no
+  static counterpart: the binary is handed over through ``AT_EXECFD`` as
+  with ``BPF_BINPRM_EXECFD``, but the argument vector is also left as the
+  caller passed it. An interpreter that loads the binary from ``AT_EXECFD``
+  then appears in ``argv[0]`` and ``/proc/pid/cmdline`` as a direct
+  execution of the binary. This is also the only way a binfmt_misc handler
+  can run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor
+  to ``execveat()``, which otherwise fails because the interpreter has no
+  path by which to open it.
 
 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.
diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index a3175521c8dd..9d7fc598d8c1 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -296,6 +296,7 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	const char *interpreter;
 	struct file *interp_file;
 	struct binfmt_misc *misc;
+	bool transparent = false;
 	bool preserve_argv0;
 	int retval;
 
@@ -307,10 +308,6 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	if (!fmt)
 		return -ENOEXEC;
 
-	/* Need to be able to load the file after exec */
-	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
-		return -ENOENT;
-
 	interpreter = entry_select_interpreter(fmt, bprm);
 	if (IS_ERR(interpreter))
 		return PTR_ERR(interpreter);
@@ -326,10 +323,13 @@ static int load_misc_binary(struct linux_binprm *bprm)
 		/* Clear so it can't accumulate into a nested interpreter level. */
 		bprm->bpf_flags = 0;
 
-		preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0;
+		transparent = f & BPF_BINPRM_TRANSPARENT;
+		/* In transparent mode argv[0] is already the caller's. */
+		preserve_argv0 = (f & BPF_BINPRM_PRESERVE_ARGV0) && !transparent;
 		if (f & BPF_BINPRM_CREDENTIALS)
 			bprm->execfd_creds = 1;
-		if (f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD))
+		if (f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD |
+			 BPF_BINPRM_TRANSPARENT))
 			bprm->have_execfd = 1;
 	} else {
 		preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0;
@@ -339,45 +339,63 @@ static int load_misc_binary(struct linux_binprm *bprm)
 			bprm->have_execfd = 1;
 	}
 
-	/* The entry's own choice - not one accumulated from an earlier level. */
-	if (preserve_argv0) {
-		bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
+	if (transparent) {
+		/*
+		 * Transparent execution: the interpreter takes the binary from
+		 * AT_EXECFD, so leave the argument vector and bprm->interp as
+		 * the caller set them. argv[0] and /proc/pid/cmdline then look
+		 * like a direct execution of the binary, and a binary passed as
+		 * an inaccessible O_CLOEXEC fd to execveat() can be run at all.
+		 */
+
+		/* No argv is built, so drop any staged interpreter argument. */
+		kfree(bprm->bpf_interp_arg);
+		bprm->bpf_interp_arg = NULL;
 	} else {
-		retval = remove_arg_zero(bprm);
-		if (retval)
-			return retval;
-	}
+		/* The interpreter has to be able to load the binary by path. */
+		if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
+			return -ENOENT;
 
-	/* make the binary the last argument to the interpreter */
-	retval = copy_string_kernel(bprm->interp, bprm);
-	if (retval < 0)
-		return retval;
-	bprm->argc++;
+		/* The entry's own choice - not one accumulated from an earlier level. */
+		if (preserve_argv0) {
+			bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
+		} else {
+			retval = remove_arg_zero(bprm);
+			if (retval)
+				return retval;
+		}
 
-	/*
-	 * A single optional argument to the interpreter, inserted between it
-	 * and the binary just like the argument of a #! interpreter line.
-	 */
-	if (bprm->bpf_interp_arg) {
-		retval = copy_string_kernel(bprm->bpf_interp_arg, bprm);
+		/* make the binary the last argument to the interpreter */
+		retval = copy_string_kernel(bprm->interp, bprm);
 		if (retval < 0)
 			return retval;
 		bprm->argc++;
-		/* Consumed - don't let it leak into a nested interpreter's argv. */
-		kfree(bprm->bpf_interp_arg);
-		bprm->bpf_interp_arg = NULL;
-	}
 
-	/* add the interp as argv[0] */
-	retval = copy_string_kernel(interpreter, bprm);
-	if (retval < 0)
-		return retval;
-	bprm->argc++;
+		/*
+		 * A single optional argument to the interpreter, inserted
+		 * between it and the binary like a #! interpreter line's.
+		 */
+		if (bprm->bpf_interp_arg) {
+			retval = copy_string_kernel(bprm->bpf_interp_arg, bprm);
+			if (retval < 0)
+				return retval;
+			bprm->argc++;
+			/* Consumed - don't leak it into a nested interpreter's argv. */
+			kfree(bprm->bpf_interp_arg);
+			bprm->bpf_interp_arg = NULL;
+		}
 
-	/* Update interp in case binfmt_script needs it. */
-	retval = bprm_change_interp(interpreter, bprm);
-	if (retval < 0)
-		return retval;
+		/* add the interp as argv[0] */
+		retval = copy_string_kernel(interpreter, bprm);
+		if (retval < 0)
+			return retval;
+		bprm->argc++;
+
+		/* Update interp in case binfmt_script needs it. */
+		retval = bprm_change_interp(interpreter, bprm);
+		if (retval < 0)
+			return retval;
+	}
 
 	if (fmt->flags & MISC_FMT_OPEN_FILE) {
 		interp_file = file_clone_open(fmt->interp_file);
diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c
index 00c787e8bdcc..fb5d0f465ccc 100644
--- a/fs/binfmt_misc_bpf.c
+++ b/fs/binfmt_misc_bpf.c
@@ -178,7 +178,9 @@ __bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
  * O flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0],
  * BPF_BINPRM_CREDENTIALS computes credentials from the binary, and
  * BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD.
- * Calling it again replaces the flags, passing zero clears them again.
+ * BPF_BINPRM_TRANSPARENT additionally leaves the argument vector untouched,
+ * making the exec look like a direct execution of the binary. Calling it
+ * again replaces the flags, passing zero clears them again.
  *
  * Return: 0 on success, -EINVAL if @flags contains an unknown bit
  */
@@ -186,7 +188,7 @@ __bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm,
 				     enum bpf_binprm_flags flags)
 {
 	if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS |
-		      BPF_BINPRM_EXECFD))
+		      BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT))
 		return -EINVAL;
 
 	bprm->bpf_flags = flags;
diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h
index d3112a00cc19..c43df749e021 100644
--- a/include/linux/binfmt_misc.h
+++ b/include/linux/binfmt_misc.h
@@ -16,6 +16,8 @@ struct user_namespace;
  * @BPF_BINPRM_CREDENTIALS: compute credentials from the binary; implies execfd
  *                          (like the 'C' flag)
  * @BPF_BINPRM_EXECFD: pass the binary via AT_EXECFD (like the 'O' flag)
+ * @BPF_BINPRM_TRANSPARENT: leave argv untouched, the interpreter takes the
+ *                          binary from AT_EXECFD; implies execfd
  *
  * Set from a load program with bpf_binprm_set_flags(). Unlike a static entry,
  * a bpf handler chooses these per exec rather than once at registration.
@@ -24,6 +26,7 @@ enum bpf_binprm_flags {
 	BPF_BINPRM_PRESERVE_ARGV0	= (1ULL << 0),
 	BPF_BINPRM_CREDENTIALS		= (1ULL << 1),
 	BPF_BINPRM_EXECFD		= (1ULL << 2),
+	BPF_BINPRM_TRANSPARENT		= (1ULL << 3),
 };
 
 /**

-- 
2.53.0



  parent reply	other threads:[~2026-07-14 19:58 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-14 19:58 [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Christian Brauner
2026-07-14 19:58 ` [PATCH v2 1/9] exec: stash bpf-selected interpreter state in struct linux_binprm Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 2/9] binfmt_misc: add binfmt_misc_ops bpf struct_ops Christian Brauner
2026-07-14 19:58 ` [PATCH v2 3/9] binfmt_misc: let the entry lookup walk sleep Christian Brauner
2026-07-14 19:58 ` [PATCH v2 4/9] binfmt_misc: wire up bpf-backed 'B' entries Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 5/9] bpf: allow fs kfuncs for binfmt_misc_ops programs Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 6/9] binfmt_misc: let bpf handlers pass an argument to the interpreter Christian Brauner
2026-07-15  0:28   ` Farid Zakaria
2026-07-14 19:58 ` [PATCH v2 7/9] binfmt_misc: let a bpf handler choose the invocation flags per exec Christian Brauner
2026-07-14 19:58 ` Christian Brauner [this message]
2026-07-14 19:58 ` [PATCH v2 9/9] selftests/exec: add binfmt_misc bpf-backed handler test Christian Brauner
2026-07-15  0:28 ` [PATCH v2 0/9] binfmt_misc: bpf-backed binary type handlers Farid Zakaria
     [not found]   ` <20260715-reorganisation-umtausch-radiergummi-8b38ca81ebb0@brauner>
2026-07-16  0:10     ` Farid Zakaria
2026-07-16  0:12       ` Farid Zakaria

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=20260714-work-bpf-binfmt_misc-v2-8-57b7529c002c@kernel.org \
    --to=brauner@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=corbet@lwn.net \
    --cc=daniel@iogearbox.net \
    --cc=farid.m.zakaria@gmail.com \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=kees@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=mail@johnericson.me \
    --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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox