On Fri, Jul 3, 2026 at 2:22 AM Christian Brauner <[email protected]> wrote:
>
> > Introduce a pluggable framework for ELF binary loading to allow dynamic
> > resolution and redirection of program interpreters (PT_INTERP). This is
> > primarily designed to support hermetic path resolution like NixOS $ORIGIN
> > relative dynamic linkers without bloating the core ELF loader or 
> > compromising
> > system execution security.
> >
> > Introduce a new registration interface for kernel modules to register
> > open_interpreter callbacks. Standard ELF loading queries this registry; if a
> > plugin resolves a custom segment type (like PT_INTERP_NIX), it returns a 
> > file
> > descriptor for the resolved interpreter. Secure execution environments
> > (bprm->secureexec) bypass relative resolution for safety.
> >
> > Signed-off-by: Farid Zakaria <[email protected]>
> >
> > diff --git a/fs/Kconfig.binfmt b/fs/Kconfig.binfmt
> > index 1949e25c7741..ef4277fd8050 100644
> > --- a/fs/Kconfig.binfmt
> > +++ b/fs/Kconfig.binfmt
> > @@ -38,6 +38,21 @@ config BINFMT_ELF_KUNIT_TEST
> >         only needed for debugging. Note that with CONFIG_COMPAT=y, the
> >         compat_binfmt_elf KUnit test is also created.
> >
> > +config BINFMT_ELF_PLUGINS
> > +     bool "Enable plugin support for ELF interpreter loading"
> > +     depends on BINFMT_ELF
> > +     help
> > +       This option allows kernel modules to register handlers to 
> > dynamically
> > +       resolve and override the ELF program interpreter (e.g. supporting 
> > relative
> > +       interpreter paths with $ORIGIN).
> > +
> > +config BINFMT_ELF_NIX
> > +     tristate "ELF interpreter plugin for NixOS ($ORIGIN support)"
> > +     depends on BINFMT_ELF_PLUGINS
> > +     help
> > +       This builds the NixOS ELF interpreter plugin. It intercepts 
> > PT_INTERP_NIX
> > +       headers to resolve relative and $ORIGIN interpreter paths.
> > +
> >  config COMPAT_BINFMT_ELF
> >       def_bool y
> >       depends on COMPAT && BINFMT_ELF
> > diff --git a/fs/Makefile b/fs/Makefile
> > index 89a8a9d207d1..bd81e7ff64f3 100644
> > --- a/fs/Makefile
> > +++ b/fs/Makefile
> > @@ -35,6 +35,7 @@ obj-$(CONFIG_FILE_LOCKING)      += locks.o
> >  obj-$(CONFIG_BINFMT_MISC)    += binfmt_misc.o
> >  obj-$(CONFIG_BINFMT_SCRIPT)  += binfmt_script.o
> >  obj-$(CONFIG_BINFMT_ELF)     += binfmt_elf.o
> > +obj-$(CONFIG_BINFMT_ELF_NIX) += binfmt_elf_nix.o
> >  obj-$(CONFIG_COMPAT_BINFMT_ELF)      += compat_binfmt_elf.o
> >  obj-$(CONFIG_BINFMT_ELF_FDPIC)       += binfmt_elf_fdpic.o
> >  obj-$(CONFIG_BINFMT_FLAT)    += binfmt_flat.o
> > diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
> > index 16a56b6b3f6c..53fa2681555a 100644
> > --- a/fs/binfmt_elf.c
> > +++ b/fs/binfmt_elf.c
> > @@ -35,6 +35,7 @@
> >  #include <linux/random.h>
> >  #include <linux/elf.h>
> >  #include <linux/elf-randomize.h>
> > +#include <linux/elf_plugins.h>
> >  #include <linux/utsname.h>
> >  #include <linux/coredump.h>
> >  #include <linux/sched.h>
> > @@ -870,6 +871,12 @@ static int load_elf_binary(struct linux_binprm *bprm)
> >       if (!elf_phdata)
> >               goto out;
> >
> > +     interpreter = elf_plugin_open_interpreter(bprm, elf_ex, elf_phdata);
> > +     if (IS_ERR(interpreter)) {
> > +             retval = PTR_ERR(interpreter);
> > +             goto out_free_ph;
> > +     }
> > +
> >       elf_ppnt = elf_phdata;
> >       for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
> >               char *elf_interpreter;
> > @@ -882,6 +889,9 @@ static int load_elf_binary(struct linux_binprm *bprm)
> >               if (elf_ppnt->p_type != PT_INTERP)
> >                       continue;
> >
> > +             if (interpreter)
> > +                     continue;
> > +
>
>
> >               /*
> >                * This is the program interpreter used for shared libraries -
> >                * for now assume that this is an a.out format binary.
> > @@ -935,6 +945,20 @@ static int load_elf_binary(struct linux_binprm *bprm)
> >               goto out_free_ph;
> >       }
> >
> > +     if (interpreter && !interp_elf_ex) {
> > +             interp_elf_ex = kmalloc_obj(*interp_elf_ex);
> > +             if (!interp_elf_ex) {
> > +                     retval = -ENOMEM;
> > +                     goto out_free_file;
> > +             }
> > +
> > +             /* Get the exec headers */
> > +             retval = elf_read(interpreter, interp_elf_ex,
> > +                               sizeof(*interp_elf_ex), 0);
> > +             if (retval < 0)
> > +                     goto out_free_dentry;
> > +     }
> > +
> >       elf_ppnt = elf_phdata;
> >       for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
> >               switch (elf_ppnt->p_type) {
> > diff --git a/fs/binfmt_elf_nix.c b/fs/binfmt_elf_nix.c
> > new file mode 100644
> > index 000000000000..d28b92c30939
> > --- /dev/null
> > +++ b/fs/binfmt_elf_nix.c
> > @@ -0,0 +1,108 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +#include <linux/module.h>
> > +#include <linux/kernel.h>
> > +#include <linux/init.h>
> > +#include <linux/fs.h>
> > +#include <linux/path.h>
> > +#include <linux/namei.h>
> > +#include <linux/elf.h>
> > +#include <linux/elf_plugins.h>
> > +#include <linux/slab.h>
> > +
> > +MODULE_DESCRIPTION("ELF Interpreter plugin for NixOS / $ORIGIN");
> > +MODULE_AUTHOR("Farid Zakaria");
> > +MODULE_LICENSE("GPL");
> > +
> > +/* Mnemonic value for NixOS-specific program interpreter: 'N', 'I', 'X', 3 
> > */
> > +#define PT_INTERP_NIX  (PT_LOOS + 0x4e49583)
> > +
> > +static struct file *nix_open_interpreter(struct linux_binprm *bprm,
> > +                                      struct elfhdr *elf_ex,
> > +                                      struct elf_phdr *elf_phdata)
> > +{
> > +     struct elf_phdr *elf_ppnt;
> > +     struct file *interpreter = NULL;
> > +     char *elf_interpreter = NULL;
> > +     int i, retval;
> > +
> > +     /* Find the custom Nix interpreter header */
> > +     elf_ppnt = elf_phdata;
> > +     for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
> > +             if (elf_ppnt->p_type == PT_INTERP_NIX)
> > +                     break;
> > +     }
>
>
> > +
> > +     if (i == elf_ex->e_phnum)
> > +             return NULL; /* Segment not present; fall back to others */
> > +
> > +     /* Security check: refuse relative interp resolution on secure 
> > execution */
> > +     if (bprm->secureexec) {
> > +             pr_warn_once("binfmt_elf_nix: secureexec active, refusing 
> > custom interpreter lookup\n");
> > +             return NULL; /* Fallback to standard PT_INTERP */
> > +     }
> > +
> > +     if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)
> > +             return ERR_PTR(-ENOEXEC);
> > +
> > +     elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
> > +     if (!elf_interpreter)
> > +             return ERR_PTR(-ENOMEM);
> > +
> > +     /* Read the interpreter path from the executable file */
> > +     retval = kernel_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz, 
> > &elf_ppnt->p_offset);
>
>
> > +     if (retval != elf_ppnt->p_filesz) {
> > +             retval = (retval < 0) ? retval : -EIO;
> > +             goto out_free;
> > +     }
> > +
> > +     if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') {
> > +             retval = -ENOEXEC;
> > +             goto out_free;
> > +     }
> > +
> > +     /* Path Resolution: Absolute vs. $ORIGIN */
> > +     if (elf_interpreter[0] == '/') {
> > +             interpreter = open_exec(elf_interpreter);
> > +     } else if (strncmp(elf_interpreter, "$ORIGIN/", 8) == 0 || 
> > strncmp(elf_interpreter, "${ORIGIN}/", 10) == 0) {
> > +             const char *rel_path = (elf_interpreter[0] == '$') ? 
> > (elf_interpreter + 8) : (elf_interpreter + 10);
>
>
> > +             struct path parent_path;
> > +
> > +             /* Reference parent directory of the executed file safely */
> > +             parent_path.mnt = mntget(bprm->file->f_path.mnt);
> > +             parent_path.dentry = dget_parent(bprm->file->f_path.dentry);
> > +
> > +             /* Open relative to parent directory */
> > +             interpreter = file_open_root(&parent_path, rel_path, 
> > O_RDONLY, 0);
>
>
> > +
> > +             path_put(&parent_path);
> > +     } else {
> > +             /* Naked relative paths are rejected for safety */
> > +             retval = -ENOEXEC;
> > +             goto out_free;
> > +     }
> > +
> > +     kfree(elf_interpreter);
> > +     return interpreter;
> > +
> > +out_free:
> > +     kfree(elf_interpreter);
> > +     return ERR_PTR(retval);
> > +}
> > +
> > +static struct elf_plugin nix_elf_plugin = {
> > +     .owner = THIS_MODULE,
> > +     .open_interpreter = nix_open_interpreter,
> > +};
> > +
> > +static int __init binfmt_elf_nix_init(void)
> > +{
> > +     return register_elf_plugin(&nix_elf_plugin);
> > +}
> > +
> > +static void __exit binfmt_elf_nix_exit(void)
> > +{
> > +     unregister_elf_plugin(&nix_elf_plugin);
> > +}
> > +
> > +module_init(binfmt_elf_nix_init);
> > +module_exit(binfmt_elf_nix_exit);
> > diff --git a/fs/exec.c b/fs/exec.c
> > index b92fe7db176c..45813bbce833 100644
> > --- a/fs/exec.c
> > +++ b/fs/exec.c
> > @@ -46,6 +46,7 @@
> >  #include <linux/key.h>
> >  #include <linux/personality.h>
> >  #include <linux/binfmts.h>
> > +#include <linux/elf_plugins.h>
> >  #include <linux/utsname.h>
> >  #include <linux/pid_namespace.h>
> >  #include <linux/module.h>
> > @@ -108,6 +109,52 @@ void unregister_binfmt(struct linux_binfmt * fmt)
> >
> >  EXPORT_SYMBOL(unregister_binfmt);
> >
> > +#if IS_ENABLED(CONFIG_BINFMT_ELF_PLUGINS)
> > +static DEFINE_MUTEX(elf_plugins_lock);
> > +static LIST_HEAD(elf_plugins);
> > +
> > +int register_elf_plugin(struct elf_plugin *plugin)
> > +{
> > +     mutex_lock(&elf_plugins_lock);
> > +     list_add_tail(&plugin->list, &elf_plugins);
> > +     mutex_unlock(&elf_plugins_lock);
> > +     return 0;
> > +}
> > +EXPORT_SYMBOL_GPL(register_elf_plugin);
> > +
> > +void unregister_elf_plugin(struct elf_plugin *plugin)
> > +{
> > +     mutex_lock(&elf_plugins_lock);
> > +     list_del(&plugin->list);
> > +     mutex_unlock(&elf_plugins_lock);
> > +}
> > +EXPORT_SYMBOL_GPL(unregister_elf_plugin);
> > +
> > +struct file *elf_plugin_open_interpreter(struct linux_binprm *bprm,
> > +                                      struct elfhdr *elf_ex,
> > +                                      struct elf_phdr *elf_phdata)
> > +{
> > +     struct elf_plugin *plugin;
> > +     struct file *file = NULL;
> > +
> > +     mutex_lock(&elf_plugins_lock);
> > +     list_for_each_entry(plugin, &elf_plugins, list) {
> > +             if (!try_module_get(plugin->owner))
> > +                     continue;
> > +             mutex_unlock(&elf_plugins_lock);
> > +
> > +             file = plugin->open_interpreter(bprm, elf_ex, elf_phdata);
>
> I have to say I do not like this at all because it also means you need
> actual kernel modules for custom binaries. Yeech.
>

Isn't this already true via binfmt for new binary formats?
ELF support in binfmt itself is also built-in from what I understand.

Anyway, I'm not too attached to the idea, I'm just trying to
brainstorm on how to get it done.

> I think you should extend binfmt_misc for that, combining it with bpf.
> When binfmt_misc selects the interpreter a bpf program is run. That bpf
> program is passed the necessary information to make a decision. It can
> check whether it's in the right sandbox. Then checks if the interpreter
> starts with the magic ORIGIN string or whatever. Once it has determined
> that a binfmt_misc entry applies things progress as usual. Then you can
> point binfmt_misc at a static binary that finds the right loader to use.
>

My audit of this shows it might be doable, but it's definitely
confusing, and I'm still not clear it solves the problem -- I will
work through an example on my NixOS machine. I think I can patch
https://github.com/nix-community/nix-ld to maybe act as this
trampoline for binfmt_misc to read PT_INTERP_NIX. I will try that and
respond back if it works conceptually at least to maybe keep the
discussion moving forward and to explore the space more.

If binfmt_misc has to point to a wrapper binary, that wrapper must
live at a fixed, hardcoded absolute path.
I guess we could bootstrap it for NixOS only but one other problem I
forsee: /proc/self/exe is now something else than expected

The entire goal of relocatability is to allow binaries to run on any
host machine without relying on global, hardcoded system paths.
Just to demo how Nix is designed to let you copy code from a machine
to another with *ideally* no assumptions.
Here is a simple hello world:

#hello.nix
with import <nixpkgs> {};
runCommandCC "hello" {} ''
  mkdir -p $out/bin
  gcc -x c -o $out/bin/hello - <<\INNER_EOF
#include <stdio.h>
int main() {
    printf("Hello, World!\n");
    return 0;
}
INNER_EOF
''

We can then build it:
> nix build -f hello.nix

My machine uses the default /nix/store prefix, so the interpreter is
set to there.
> patchelf --print-interpreter ./result/bin/hello
/nix/store/7nbi22pcc92y2fqbkyp7h3srvvklmckb-glibc-2.40-224/lib/ld-linux-x86-64.so.2

If I were to use a machine with a different prefix
/home/fmzakari/.local/nix then I couldn't "substitute" (download from
the cache), this binary.
I would have to build it and the whole graph down to our bootstrap
seeds with the new store prefix.

Nix then let's us then query the graph and copy it, including ld.so
and glibc to any other machine to ideally run.

> nix-store --query --tree $(realpath result)
/nix/store/adbwpmzwgdcmk5qcskyqwygmxbkffb32-hello
├───/nix/store/7nbi22pcc92y2fqbkyp7h3srvvklmckb-glibc-2.40-224
│   ├───/nix/store/fv5lgysa3hmf3l3dkkpwvndcg6xwhy8m-xgcc-14.3.0-libgcc
│   ├───/nix/store/qywg7bxskvihq62ms2g51fkzkrdnyfkh-libidn2-2.3.8
│   │   ├───/nix/store/hjwppd89fk8781xl4r35xqlddwqi5f66-libunistring-1.4.1
│   │   │   └───/nix/store/hjwppd89fk8781xl4r35xqlddwqi5f66-libunistring-1.4.1
[...]
│   │   └───/nix/store/qywg7bxskvihq62ms2g51fkzkrdnyfkh-libidn2-2.3.8 [...]
│   └───/nix/store/7nbi22pcc92y2fqbkyp7h3srvvklmckb-glibc-2.40-224 [...]
├───/nix/store/ybp235ps7m4yd85v0pgvqkhd4xmxf6jq-gcc-14.3.0-lib
│   ├───/nix/store/7nbi22pcc92y2fqbkyp7h3srvvklmckb-glibc-2.40-224 [...]
│   ├───/nix/store/phpq5h7cp6y1w84aysy29548irqy5dd9-gcc-14.3.0-libgcc
│   └───/nix/store/ybp235ps7m4yd85v0pgvqkhd4xmxf6jq-gcc-14.3.0-lib [...]
└───/nix/store/adbwpmzwgdcmk5qcskyqwygmxbkffb32-hello [...]

I appreciate you engaging me on this topic -- thank you.

> --
> Christian Brauner <[email protected]>

Reply via email to