[PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers

Farid Zakaria posted 5 patches 1 week, 6 days ago
Documentation/admin-guide/binfmt-misc.rst        |  40 +++-
fs/Kconfig.binfmt                                |  14 ++
fs/Makefile                                      |   1 +
fs/binfmt_misc.c                                 | 149 +++++++++++-
fs/binfmt_misc_bpf.c                             | 275 +++++++++++++++++++++++
fs/bpf_fs_kfuncs.c                               |  23 +-
fs/exec.c                                        |   1 +
include/linux/binfmt_misc.h                      |  49 ++++
include/linux/binfmts.h                          |   1 +
tools/testing/selftests/exec/Makefile            |  37 +++
tools/testing/selftests/exec/binfmt_bpf_app.c    |  12 +
tools/testing/selftests/exec/binfmt_bpf_interp.c |  15 ++
tools/testing/selftests/exec/binfmt_misc_bpf.c   | 260 +++++++++++++++++++++
tools/testing/selftests/exec/bpf_interp.bpf.c    |  52 +++++
tools/testing/selftests/exec/nix_origin.bpf.c    | 179 +++++++++++++++
15 files changed, 1091 insertions(+), 17 deletions(-)
[PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers
Posted by Farid Zakaria 1 week, 6 days ago
This is a continuation of Christian's bpf-backed binfmt_misc POC [1], which he offered
to hand off to me. I am carrying it forward. The kernel design is his and is
unchanged. This series rebases it onto his binfmt_misc locking/cleanup series [2]
and adds selftests (+ fixed from the one's shared), and therefore does
not apply to mainline alone.

As for motivation for this whole change, Christian did a great VL;MR;
write-up [1].

TL;DR: binfmt_misc can match a binary and hand it to a fixed interpreter,
but it can't match programmatically or compute the interpreter per binary.
The Nix community would love to support relocatable binaries, where the
correct loader can only be found relative to the binary itself.
This adds a 'B' handler type: a binfmt_misc_ops struct_ops program that
inspects the binary and picks the interpreter with one new kfunc,
bpf_binprm_set_interp(). 

  bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
  echo ':nix-origin:B:nix_origin::::' > /proc/sys/fs/binfmt_misc/register

Patch 5's nix_origin.bpf.c is Christian's example program with a small fix
so it passes the verifier. Christian, happy to add a Co-developed-by: and your
Signed-off-by: if you would like the credit in the trailer.

For those that want to validate this functionality on a NixOS machine,
you may find my flake.nix handy [3]. It builds the kernel, compiles the
bpf objects against its BTF, and runs the selftest on boot,

  # be sure to change virtualisation.sharedDirectories in flake.nix to your checkout
  $ nix build .#nixosConfigurations.micro-vm.config.system.build.vm -o /tmp/vm
  $ NIXPKGS_QEMU_KERNEL_micro_vm=$PWD/arch/x86/boot/bzImage \
  $ NIX_DISK_IMAGE=/tmp/vm.qcow2 \
  $ QEMU_KERNEL_PARAMS="binfmt_autotest console=ttyS0" \
  $ QEMU_OPTS="-nographic -no-reboot" \
  /tmp/vm/bin/run-micro-vm-vm

[1] https://lore.kernel.org/r/20260707-work-bpf-binfmt_misc-v1-0-74b995c84ec1@kernel.org
[2] https://lore.kernel.org/all/20260710-work-binfmt_misc-locking-v3-0-a162f7cb58d6@kernel.org/
[3] https://gist.github.com/fzakaria/0155e11a0882bd3d6e63f4070e7fac0c

To: Christian Brauner <brauner@kernel.org>
To: Alexei Starovoitov <ast@kernel.org>
To: Daniel Borkmann <daniel@iogearbox.net>
To: Martin KaFai Lau <martin.lau@linux.dev>
To: Shuah Khan <shuah@kernel.org>
Cc: Andrii Nakryiko <andrii@kernel.org>
Cc: Kees Cook <kees@kernel.org>
Cc: Alexander Viro <viro@zeniv.linux.org.uk>
Cc: Jan Kara <jack@suse.cz>
Cc: Jonathan Corbet <corbet@lwn.net>
Cc: Jann Horn <jannh@google.com>
Cc: John Ericson <mail@johnericson.me>
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-mm@kvack.org
Cc: linux-kernel@vger.kernel.org
Cc: bpf@vger.kernel.org
Cc: linux-doc@vger.kernel.org
Cc: linux-kselftest@vger.kernel.org

Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---
Changes in v2:
- Patch 5 adds new tests that validate the functionality:
  - a bpf handler matches a synthetic aarch64 header and routes it to a fixed
     interpreter the program chooses;
  - a handler resolves a "$ORIGIN/..."-relative PT_INTERP to an
     interpreter co-located with the binary (i.e, Nix usecase);
- Patch 3 is rebased to fit the new binfmt_misc code style: the 'B' field parsing is now a
  parse_bpf_fields() helper matching the new parse_{magic,extension}_fields()
  split, and load_misc_binary() keeps the bpf retry loop in the
  __free() cleanup style.
- Link to v1: https://lore.kernel.org/r/20260707-work-bpf-binfmt_misc-v1-0-74b995c84ec1@kernel.org

---
Christian Brauner (4):
      exec: stash a bpf-selected interpreter in struct linux_binprm
      binfmt_misc: add binfmt_misc_ops bpf struct_ops
      binfmt_misc: wire up bpf-backed 'B' entries
      bpf: allow fs kfuncs for binfmt_misc_ops programs

Farid Zakaria (1):
      selftests/exec: add binfmt_misc bpf-backed handler test

 Documentation/admin-guide/binfmt-misc.rst        |  40 +++-
 fs/Kconfig.binfmt                                |  14 ++
 fs/Makefile                                      |   1 +
 fs/binfmt_misc.c                                 | 149 +++++++++++-
 fs/binfmt_misc_bpf.c                             | 275 +++++++++++++++++++++++
 fs/bpf_fs_kfuncs.c                               |  23 +-
 fs/exec.c                                        |   1 +
 include/linux/binfmt_misc.h                      |  49 ++++
 include/linux/binfmts.h                          |   1 +
 tools/testing/selftests/exec/Makefile            |  37 +++
 tools/testing/selftests/exec/binfmt_bpf_app.c    |  12 +
 tools/testing/selftests/exec/binfmt_bpf_interp.c |  15 ++
 tools/testing/selftests/exec/binfmt_misc_bpf.c   | 260 +++++++++++++++++++++
 tools/testing/selftests/exec/bpf_interp.bpf.c    |  52 +++++
 tools/testing/selftests/exec/nix_origin.bpf.c    | 179 +++++++++++++++
 15 files changed, 1091 insertions(+), 17 deletions(-)
---
base-commit: 6203a47c1d78f948cd5e9ad1a4c089745e466870
change-id: 20260711-binfmt-misc-bpf-v2-0f5c1f99b9ed

Best regards,
-- 
Farid Zakaria <farid.m.zakaria@gmail.com>
Re: [PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers
Posted by Christian Brauner 1 week, 6 days ago
> This is a continuation of Christian's bpf-backed binfmt_misc POC [1], which he offered
> to hand off to me. I am carrying it forward. The kernel design is his and is
> unchanged. This series rebases it onto his binfmt_misc locking/cleanup series [2]
> and adds selftests (+ fixed from the one's shared), and therefore does
> not apply to mainline alone.
> 
> As for motivation for this whole change, Christian did a great VL;MR;
> write-up [1].
> 
> TL;DR: binfmt_misc can match a binary and hand it to a fixed interpreter,
> but it can't match programmatically or compute the interpreter per binary.
> The Nix community would love to support relocatable binaries, where the
> correct loader can only be found relative to the binary itself.
> This adds a 'B' handler type: a binfmt_misc_ops struct_ops program that
> inspects the binary and picks the interpreter with one new kfunc,
> bpf_binprm_set_interp().
> 
>   bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
>   echo ':nix-origin:B:nix_origin::::' > /proc/sys/fs/binfmt_misc/register

Thanks! So I've spent some more time working on this because I had some
future ideas that I needed to make sure could be accomodated by this
(currently on vacation but hey...). So the version I have givex this a
better design and has future extensibility in mind:

- The single sleepable load op is split into two ops:

      struct binfmt_misc_ops {
      	bool (*match)(struct linux_binprm *bprm);
      	int (*load)(struct linux_binprm *bprm);
      	char name[BINFMT_MISC_OPS_NAME_MAX];
      };

  The non-sleepable match program runs from the RCU entry lookup
  walk itself, exactly like magic and extension matching: same
  registration order, same first-match-wins semantics, and it can
  only rely on the prefetched bprm->buf. It must be free of side
  effects since the walk may be restarted.

  The sleepable load program runs once the walk has committed to the
  entry and does the file reading and the interpreter selection. Both
  ops are mandatory, the struct_ops plumbing enforces the sleepability
  of each, and since bpf_binprm_set_interp() is KF_SLEEPABLE a match
  program cannot select an interpreter by construction.

- A match is a commitment. A failing load program fails the exec instead
  of falling through to later entries. -ENOEXEC keeps its usual meaning
  and hands the binary to the remaining binary formats, for a handler
  that discovers it cannot serve the binary after all.

  This kills the part of v1 I disliked the most: the skip cursor and the
  leave-and-rescan loop in load_misc_binary() are gone. The walk is
  never left and re-entered, and 'B' entries need no special semantics
  against concurrent (un)registration anymore.

  Your v2 changelog note about keeping the bpf retry loop in the
  __free() style is moot as a consequence. The loop no longer exists.

- The load return convention flipped: 0 now means success after the
  program called bpf_binprm_set_interp(). Returning 0 without having
  selected an interpreter or returning a positive value is treated as
  -ENOEXEC, other negative errnos fail the exec.

  So the "return bpf_binprm_set_interp(...) ?: 1" idiom from the v1-era
  programs becomes plain "return bpf_binprm_set_interp(...)".

- The handler name moved from the offset field to the interpreter field
  that field consistently names whoever supplies the interpreter, a path
  for static entries, a handler for 'B' entries. Offset, magic, and mask
  must be empty:

	echo ':nix-origin:B::::nix_origin:' > register

- 'C' is allowed now, v1 rejected it. It behaves exactly as for a static
   entry. The setuid transition stays gated by vfsuid_has_mapping() in
   the caller's user namespace. Which makes 'B' handlers usable for a
   per-binary loader over setuid binaries. 'F' stays rejected as there
   is no fixed interpreter to pre-open (I have other ideas how we'll do
   something like it later.).

Nothing changed in the exec patch, the fs kfuncs patch, the kfunc
itself, or the registry/namespacing model.

I can send v3 in a bit if that's ok.

-- 
Christian Brauner <brauner@kernel.org>
Re: [PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers
Posted by Farid Zakaria 1 week, 5 days ago
On Sun Jul 12, 2026 at 5:32 AM PDT, Christian Brauner wrote:
>> This is a continuation of Christian's bpf-backed binfmt_misc POC [1], which he offered
>> to hand off to me. I am carrying it forward. The kernel design is his and is
>> unchanged. This series rebases it onto his binfmt_misc locking/cleanup series [2]
>> and adds selftests (+ fixed from the one's shared), and therefore does
>> not apply to mainline alone.
>> 
>> As for motivation for this whole change, Christian did a great VL;MR;
>> write-up [1].
>> 
>> TL;DR: binfmt_misc can match a binary and hand it to a fixed interpreter,
>> but it can't match programmatically or compute the interpreter per binary.
>> The Nix community would love to support relocatable binaries, where the
>> correct loader can only be found relative to the binary itself.
>> This adds a 'B' handler type: a binfmt_misc_ops struct_ops program that
>> inspects the binary and picks the interpreter with one new kfunc,
>> bpf_binprm_set_interp().
>> 
>>   bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
>>   echo ':nix-origin:B:nix_origin::::' > /proc/sys/fs/binfmt_misc/register
>
> Thanks! So I've spent some more time working on this because I had some
> future ideas that I needed to make sure could be accomodated by this
> (currently on vacation but hey...). So the version I have givex this a
> better design and has future extensibility in mind:
>

I also enjoy hacking over my vacation. I'm often told to take break but
some of the best ideas come when on vacation :)

> - The single sleepable load op is split into two ops:
>
>       struct binfmt_misc_ops {
>       	bool (*match)(struct linux_binprm *bprm);
>       	int (*load)(struct linux_binprm *bprm);
>       	char name[BINFMT_MISC_OPS_NAME_MAX];
>       };
>
>   The non-sleepable match program runs from the RCU entry lookup
>   walk itself, exactly like magic and extension matching: same
>   registration order, same first-match-wins semantics, and it can
>   only rely on the prefetched bprm->buf. It must be free of side
>   effects since the walk may be restarted.
>

Is relying on brpm->buf enough?
Right now that's only 256 bytes I think, which is not enough to read
segments we might care about. That worked fine when it was just a magic
number but the idea with the eBPF program is to make decisions based on
more data.

In the selftest I provided, the `PT_INTERP` segment is already at file
offset 0x318 (792). I was imaginging NixOS having to support
`PT_INTERP_NIX` in order for the produced binaries to be backwards
compatible with older kernels.

The other idea would be to make the `match()` broad and select
everything but then nearly all ELF64 binaries would match and then pay
the price to `load()` and ultimately `-ENOEXEC`. Seems like it would
make multiple BPF binfmt programs less useful.


>   The sleepable load program runs once the walk has committed to the
>   entry and does the file reading and the interpreter selection. Both
>   ops are mandatory, the struct_ops plumbing enforces the sleepability
>   of each, and since bpf_binprm_set_interp() is KF_SLEEPABLE a match
>   program cannot select an interpreter by construction.
>
> - A match is a commitment. A failing load program fails the exec instead
>   of falling through to later entries. -ENOEXEC keeps its usual meaning
>   and hands the binary to the remaining binary formats, for a handler
>   that discovers it cannot serve the binary after all.
>
>   This kills the part of v1 I disliked the most: the skip cursor and the
>   leave-and-rescan loop in load_misc_binary() are gone. The walk is
>   never left and re-entered, and 'B' entries need no special semantics
>   against concurrent (un)registration anymore.
>
>   Your v2 changelog note about keeping the bpf retry loop in the
>   __free() style is moot as a consequence. The loop no longer exists.
>
> - The load return convention flipped: 0 now means success after the
>   program called bpf_binprm_set_interp(). Returning 0 without having
>   selected an interpreter or returning a positive value is treated as
>   -ENOEXEC, other negative errnos fail the exec.
>
>   So the "return bpf_binprm_set_interp(...) ?: 1" idiom from the v1-era
>   programs becomes plain "return bpf_binprm_set_interp(...)".
>
> - The handler name moved from the offset field to the interpreter field
>   that field consistently names whoever supplies the interpreter, a path
>   for static entries, a handler for 'B' entries. Offset, magic, and mask
>   must be empty:
>
> 	echo ':nix-origin:B::::nix_origin:' > register
>
> - 'C' is allowed now, v1 rejected it. It behaves exactly as for a static
>    entry. The setuid transition stays gated by vfsuid_has_mapping() in
>    the caller's user namespace. Which makes 'B' handlers usable for a
>    per-binary loader over setuid binaries. 'F' stays rejected as there
>    is no fixed interpreter to pre-open (I have other ideas how we'll do
>    something like it later.).
>
> Nothing changed in the exec patch, the fs kfuncs patch, the kfunc
> itself, or the registry/namespacing model.
>
> I can send v3 in a bit if that's ok.

I don't mind at all you sending v3 and in fact I've been enjoying your
involvement. I didn't know what to expect when I offered this idea up to
the community. 

I'm happy to keep co-developing this with you within a design you feel
acceptable with. Please let me know how I can remain engaged and
helpful.

One last thing I was thinking about is that we will also need to support
$ORIGIN in the shebang path, however I just tested it and this current broad
BPF solution can largely handle it [1], with a small wrinkle.

  $ printf '#!$ORIGIN/interp\n' > /opt/app/greet
  $ cp ./interp /opt/app/interp     # any interpreter/loader
  $ chmod +x /opt/app/greet /opt/app/interp

  # stock kernel: binfmt_script opens the literal "$ORIGIN/interp"
  $ /opt/app/greet
  bash: /opt/app/greet: $ORIGIN/interp: bad interpreter: No such file or directory

  # with the handler registered, $ORIGIN resolves to the script's dir
  $ bpftool struct_ops register shebang_origin.bpf.o /sys/fs/bpf
  $ echo ':shebang-origin:B:shebang_origin::::' > /proc/sys/fs/binfmt_misc/register
  $ /opt/app/greet
  <runs /opt/app/interp, the loader found next to the script>

The wrinkle is that it can't express today is the single optional argument.
(i.e. `#!interp arg" -> argv[1]=arg`). We might ned a way to express
that in the load.

Anywyas, thanks again. All your ideas made sense modulo I'm unsure if
`bprm->buf` is enough to make a decision...

[1] https://gist.github.com/fzakaria/2e1e1c44fa488a951674f8761c672366
Re: [PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers
Posted by Christian Brauner 1 week, 5 days ago
> > - The single sleepable load op is split into two ops:
> >
> >       struct binfmt_misc_ops {
> >       	bool (*match)(struct linux_binprm *bprm);
> >       	int (*load)(struct linux_binprm *bprm);
> >       	char name[BINFMT_MISC_OPS_NAME_MAX];
> >       };
> >
> >   The non-sleepable match program runs from the RCU entry lookup
> >   walk itself, exactly like magic and extension matching: same
> >   registration order, same first-match-wins semantics, and it can
> >   only rely on the prefetched bprm->buf. It must be free of side
> >   effects since the walk may be restarted.
> >
> 
> Is relying on brpm->buf enough?
> Right now that's only 256 bytes I think, which is not enough to read
> segments we might care about. That worked fine when it was just a magic
> number but the idea with the eBPF program is to make decisions based on
> more data.
> 
> In the selftest I provided, the `PT_INTERP` segment is already at file
> offset 0x318 (792). I was imaginging NixOS having to support
> `PT_INTERP_NIX` in order for the produced binaries to be backwards
> compatible with older kernels.
> 
> The other idea would be to make the `match()` broad and select
> everything but then nearly all ELF64 binaries would match and then pay
> the price to `load()` and ultimately `-ENOEXEC`. Seems like it would
> make multiple BPF binfmt programs less useful.

Ok, so your idea is to have multiple bpf programs that look for
different interpreters. Yeah, then you have to be able to sleep because
you need to be able to fault. It makes the code uglier but I can see how
that's useful. I'll see how nice I can make that.

> >   The sleepable load program runs once the walk has committed to the
> >   entry and does the file reading and the interpreter selection. Both
> >   ops are mandatory, the struct_ops plumbing enforces the sleepability
> >   of each, and since bpf_binprm_set_interp() is KF_SLEEPABLE a match
> >   program cannot select an interpreter by construction.
> >
> > - A match is a commitment. A failing load program fails the exec instead
> >   of falling through to later entries. -ENOEXEC keeps its usual meaning
> >   and hands the binary to the remaining binary formats, for a handler
> >   that discovers it cannot serve the binary after all.
> >
> >   This kills the part of v1 I disliked the most: the skip cursor and the
> >   leave-and-rescan loop in load_misc_binary() are gone. The walk is
> >   never left and re-entered, and 'B' entries need no special semantics
> >   against concurrent (un)registration anymore.
> >
> >   Your v2 changelog note about keeping the bpf retry loop in the
> >   __free() style is moot as a consequence. The loop no longer exists.
> >
> > - The load return convention flipped: 0 now means success after the
> >   program called bpf_binprm_set_interp(). Returning 0 without having
> >   selected an interpreter or returning a positive value is treated as
> >   -ENOEXEC, other negative errnos fail the exec.
> >
> >   So the "return bpf_binprm_set_interp(...) ?: 1" idiom from the v1-era
> >   programs becomes plain "return bpf_binprm_set_interp(...)".
> >
> > - The handler name moved from the offset field to the interpreter field
> >   that field consistently names whoever supplies the interpreter, a path
> >   for static entries, a handler for 'B' entries. Offset, magic, and mask
> >   must be empty:
> >
> > 	echo ':nix-origin:B::::nix_origin:' > register
> >
> > - 'C' is allowed now, v1 rejected it. It behaves exactly as for a static
> >    entry. The setuid transition stays gated by vfsuid_has_mapping() in
> >    the caller's user namespace. Which makes 'B' handlers usable for a
> >    per-binary loader over setuid binaries. 'F' stays rejected as there
> >    is no fixed interpreter to pre-open (I have other ideas how we'll do
> >    something like it later.).
> >
> > Nothing changed in the exec patch, the fs kfuncs patch, the kfunc
> > itself, or the registry/namespacing model.
> >
> > I can send v3 in a bit if that's ok.
> 
> I don't mind at all you sending v3 and in fact I've been enjoying your
> involvement. I didn't know what to expect when I offered this idea up to
> the community. 
> 
> I'm happy to keep co-developing this with you within a design you feel
> acceptable with. Please let me know how I can remain engaged and
> helpful.
> 
> One last thing I was thinking about is that we will also need to support
> $ORIGIN in the shebang path, however I just tested it and this current broad
> BPF solution can largely handle it [1], with a small wrinkle.
> 
>   $ printf '#!$ORIGIN/interp\n' > /opt/app/greet
>   $ cp ./interp /opt/app/interp     # any interpreter/loader
>   $ chmod +x /opt/app/greet /opt/app/interp
> 
>   # stock kernel: binfmt_script opens the literal "$ORIGIN/interp"
>   $ /opt/app/greet
>   bash: /opt/app/greet: $ORIGIN/interp: bad interpreter: No such file or directory
> 
>   # with the handler registered, $ORIGIN resolves to the script's dir
>   $ bpftool struct_ops register shebang_origin.bpf.o /sys/fs/bpf
>   $ echo ':shebang-origin:B:shebang_origin::::' > /proc/sys/fs/binfmt_misc/register
>   $ /opt/app/greet
>   <runs /opt/app/interp, the loader found next to the script>
> 
> The wrinkle is that it can't express today is the single optional argument.
> (i.e. `#!interp arg" -> argv[1]=arg`). We might ned a way to express
> that in the load.

Ah, fun. binfmt_misc wasn't able to express this at all. It's good to
close that gap. This can just be done by adding
bpf_binprm_set_interp_arg().
Re: [PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers
Posted by Farid Zakaria 1 week, 5 days ago
On Mon Jul 13, 2026 at 1:20 AM PDT, Christian Brauner wrote:
>> > - The single sleepable load op is split into two ops:
>> >
>> >       struct binfmt_misc_ops {
>> >       	bool (*match)(struct linux_binprm *bprm);
>> >       	int (*load)(struct linux_binprm *bprm);
>> >       	char name[BINFMT_MISC_OPS_NAME_MAX];
>> >       };
>> >
>> >   The non-sleepable match program runs from the RCU entry lookup
>> >   walk itself, exactly like magic and extension matching: same
>> >   registration order, same first-match-wins semantics, and it can
>> >   only rely on the prefetched bprm->buf. It must be free of side
>> >   effects since the walk may be restarted.
>> >
>> 
>> Is relying on brpm->buf enough?
>> Right now that's only 256 bytes I think, which is not enough to read
>> segments we might care about. That worked fine when it was just a magic
>> number but the idea with the eBPF program is to make decisions based on
>> more data.
>> 
>> In the selftest I provided, the `PT_INTERP` segment is already at file
>> offset 0x318 (792). I was imaginging NixOS having to support
>> `PT_INTERP_NIX` in order for the produced binaries to be backwards
>> compatible with older kernels.
>> 
>> The other idea would be to make the `match()` broad and select
>> everything but then nearly all ELF64 binaries would match and then pay
>> the price to `load()` and ultimately `-ENOEXEC`. Seems like it would
>> make multiple BPF binfmt programs less useful.
>
> Ok, so your idea is to have multiple bpf programs that look for
> different interpreters. Yeah, then you have to be able to sleep because
> you need to be able to fault. It makes the code uglier but I can see how
> that's useful. I'll see how nice I can make that.
>

I mean... I thought that's the intent to binfmt_misc, to have multiple
handlers -- with the expressiveness of eBPF vs. the limited
magic/extension, it seems like a natural extension to support multiple.
Otherwise anyone on NixOS would be limited to our single handler for all
binaries which seems not ideal?

I'm not knowledgeable enough to comment on the sleep/faulting aspect so
I leave that to your expertise.

>> >   The sleepable load program runs once the walk has committed to the
>> >   entry and does the file reading and the interpreter selection. Both
>> >   ops are mandatory, the struct_ops plumbing enforces the sleepability
>> >   of each, and since bpf_binprm_set_interp() is KF_SLEEPABLE a match
>> >   program cannot select an interpreter by construction.
>> >
>> > - A match is a commitment. A failing load program fails the exec instead
>> >   of falling through to later entries. -ENOEXEC keeps its usual meaning
>> >   and hands the binary to the remaining binary formats, for a handler
>> >   that discovers it cannot serve the binary after all.
>> >
>> >   This kills the part of v1 I disliked the most: the skip cursor and the
>> >   leave-and-rescan loop in load_misc_binary() are gone. The walk is
>> >   never left and re-entered, and 'B' entries need no special semantics
>> >   against concurrent (un)registration anymore.
>> >
>> >   Your v2 changelog note about keeping the bpf retry loop in the
>> >   __free() style is moot as a consequence. The loop no longer exists.
>> >
>> > - The load return convention flipped: 0 now means success after the
>> >   program called bpf_binprm_set_interp(). Returning 0 without having
>> >   selected an interpreter or returning a positive value is treated as
>> >   -ENOEXEC, other negative errnos fail the exec.
>> >
>> >   So the "return bpf_binprm_set_interp(...) ?: 1" idiom from the v1-era
>> >   programs becomes plain "return bpf_binprm_set_interp(...)".
>> >
>> > - The handler name moved from the offset field to the interpreter field
>> >   that field consistently names whoever supplies the interpreter, a path
>> >   for static entries, a handler for 'B' entries. Offset, magic, and mask
>> >   must be empty:
>> >
>> > 	echo ':nix-origin:B::::nix_origin:' > register
>> >
>> > - 'C' is allowed now, v1 rejected it. It behaves exactly as for a static
>> >    entry. The setuid transition stays gated by vfsuid_has_mapping() in
>> >    the caller's user namespace. Which makes 'B' handlers usable for a
>> >    per-binary loader over setuid binaries. 'F' stays rejected as there
>> >    is no fixed interpreter to pre-open (I have other ideas how we'll do
>> >    something like it later.).
>> >
>> > Nothing changed in the exec patch, the fs kfuncs patch, the kfunc
>> > itself, or the registry/namespacing model.
>> >
>> > I can send v3 in a bit if that's ok.
>> 
>> I don't mind at all you sending v3 and in fact I've been enjoying your
>> involvement. I didn't know what to expect when I offered this idea up to
>> the community. 
>> 
>> I'm happy to keep co-developing this with you within a design you feel
>> acceptable with. Please let me know how I can remain engaged and
>> helpful.
>> 
>> One last thing I was thinking about is that we will also need to support
>> $ORIGIN in the shebang path, however I just tested it and this current broad
>> BPF solution can largely handle it [1], with a small wrinkle.
>> 
>>   $ printf '#!$ORIGIN/interp\n' > /opt/app/greet
>>   $ cp ./interp /opt/app/interp     # any interpreter/loader
>>   $ chmod +x /opt/app/greet /opt/app/interp
>> 
>>   # stock kernel: binfmt_script opens the literal "$ORIGIN/interp"
>>   $ /opt/app/greet
>>   bash: /opt/app/greet: $ORIGIN/interp: bad interpreter: No such file or directory
>> 
>>   # with the handler registered, $ORIGIN resolves to the script's dir
>>   $ bpftool struct_ops register shebang_origin.bpf.o /sys/fs/bpf
>>   $ echo ':shebang-origin:B:shebang_origin::::' > /proc/sys/fs/binfmt_misc/register
>>   $ /opt/app/greet
>>   <runs /opt/app/interp, the loader found next to the script>
>> 
>> The wrinkle is that it can't express today is the single optional argument.
>> (i.e. `#!interp arg" -> argv[1]=arg`). We might ned a way to express
>> that in the load.
>
> Ah, fun. binfmt_misc wasn't able to express this at all. It's good to
> close that gap. This can just be done by adding
> bpf_binprm_set_interp_arg().

What is "cool" about this is now it's also a net-new feature for shebags
which the "old" binfmt_misc didn't support. Anyways, I was thining ahead
about what I would need to support $ORIGIN fully and this was
a remaining gap. We also explored this idea in a patch pre-BPF [1] but
now that we've settled on this solution it seems much more natural.


I was recently reached out by some Bazel people who are also interested
in this $ORIGIN support. Interestingly they let me know that
IllumOS/Solaris had support for it as well.

[1] https://github.com/alurm/relocatable-shebangs/blob/18db0da6cd69ce19bb0273f2548a2d96bdaa82b1/relocatable-shebangs.patch
Re: [PATCH v2 0/5] binfmt_misc: bpf-backed binary type handlers
Posted by Christian Brauner 1 week, 5 days ago
On 2026-07-13 08:19 -0700, Farid Zakaria wrote:
> On Mon Jul 13, 2026 at 1:20 AM PDT, Christian Brauner wrote:
> >> > - The single sleepable load op is split into two ops:
> >> >
> >> >       struct binfmt_misc_ops {
> >> >       	bool (*match)(struct linux_binprm *bprm);
> >> >       	int (*load)(struct linux_binprm *bprm);
> >> >       	char name[BINFMT_MISC_OPS_NAME_MAX];
> >> >       };
> >> >
> >> >   The non-sleepable match program runs from the RCU entry lookup
> >> >   walk itself, exactly like magic and extension matching: same
> >> >   registration order, same first-match-wins semantics, and it can
> >> >   only rely on the prefetched bprm->buf. It must be free of side
> >> >   effects since the walk may be restarted.
> >> >
> >> 
> >> Is relying on brpm->buf enough?
> >> Right now that's only 256 bytes I think, which is not enough to read
> >> segments we might care about. That worked fine when it was just a magic
> >> number but the idea with the eBPF program is to make decisions based on
> >> more data.
> >> 
> >> In the selftest I provided, the `PT_INTERP` segment is already at file
> >> offset 0x318 (792). I was imaginging NixOS having to support
> >> `PT_INTERP_NIX` in order for the produced binaries to be backwards
> >> compatible with older kernels.
> >> 
> >> The other idea would be to make the `match()` broad and select
> >> everything but then nearly all ELF64 binaries would match and then pay
> >> the price to `load()` and ultimately `-ENOEXEC`. Seems like it would
> >> make multiple BPF binfmt programs less useful.
> >
> > Ok, so your idea is to have multiple bpf programs that look for
> > different interpreters. Yeah, then you have to be able to sleep because
> > you need to be able to fault. It makes the code uglier but I can see how
> > that's useful. I'll see how nice I can make that.
> >
> 
> I mean... I thought that's the intent to binfmt_misc, to have multiple
> handlers -- with the expressiveness of eBPF vs. the limited

I was thinking of having one bpf program per type (elf, python, etc) but
I can see how it's useful to have multiple elf bpf handlers...

> > Ah, fun. binfmt_misc wasn't able to express this at all. It's good to
> > close that gap. This can just be done by adding
> > bpf_binprm_set_interp_arg().
> 
> What is "cool" about this is now it's also a net-new feature for shebags
> which the "old" binfmt_misc didn't support. Anyways, I was thining ahead
> about what I would need to support $ORIGIN fully and this was
> a remaining gap. We also explored this idea in a patch pre-BPF [1] but
> now that we've settled on this solution it seems much more natural.
> 
> 
> I was recently reached out by some Bazel people who are also interested
> in this $ORIGIN support. Interestingly they let me know that

Yeah, for build systemd it's very nice ofc.

> IllumOS/Solaris had support for it as well.

The whole $ORIGIN thing comes from Illumos/Solaris afair. So no big
surprise.