On 2026-07-03 11:32 -0700, Farid Zakaria wrote: > 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.
This was just an example. The bpf program can calculate a path however it sees fit including making use of ORIGIN or whatever.

