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. Assisted-by: Gemini Signed-off-by: Farid Zakaria --- Hey Christian, Here is a rough draft of what I thought something could look like for a pluggable ELF interpreter loader. I've included the relocatable loader in the patch but this can be out-of-tree if needed; it's also named "Nix" to distinguish the need for it. I chose to read from a distinct segment type (PT_INTERP_NIX) to avoid backwards incompatibility. The default loader itself could be a plugin similar to binfmt_elf, but for now I wanted the patch to be small to demonstrate the concept. fs/Kconfig.binfmt | 15 +++++ fs/Makefile | 1 + fs/binfmt_elf.c | 24 ++++++++ fs/binfmt_elf_nix.c | 108 ++++++++++++++++++++++++++++++++++++ fs/exec.c | 47 ++++++++++++++++ include/linux/elf_plugins.h | 39 +++++++++++++ 6 files changed, 234 insertions(+) create mode 100644 fs/binfmt_elf_nix.c create mode 100644 include/linux/elf_plugins.h diff --git a/fs/Kconfig.binfmt b/fs/Kconfig.binfmt index 1949e25c7..ef4277fd8 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 89a8a9d20..bd81e7ff6 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 16a56b6b3..53fa26815 100644 --- a/fs/binfmt_elf.c +++ b/fs/binfmt_elf.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -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 000000000..d28b92c30 --- /dev/null +++ b/fs/binfmt_elf_nix.c @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 b92fe7db1..45813bbce 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -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); + + mutex_lock(&elf_plugins_lock); + module_put(plugin->owner); + if (file) + break; + } + mutex_unlock(&elf_plugins_lock); + return file; +} +#endif + static inline void put_binfmt(struct linux_binfmt * fmt) { module_put(fmt->module); diff --git a/include/linux/elf_plugins.h b/include/linux/elf_plugins.h new file mode 100644 index 000000000..826a32854 --- /dev/null +++ b/include/linux/elf_plugins.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef _LINUX_ELF_PLUGINS_H +#define _LINUX_ELF_PLUGINS_H + +#include +#include +#include + +struct elf_plugin { + struct list_head list; + struct module *owner; + struct file *(*open_interpreter)(struct linux_binprm *bprm, + struct elfhdr *elf_ex, + struct elf_phdr *elf_phdata); +}; + +#if IS_ENABLED(CONFIG_BINFMT_ELF_PLUGINS) +int register_elf_plugin(struct elf_plugin *plugin); +void unregister_elf_plugin(struct elf_plugin *plugin); +struct file *elf_plugin_open_interpreter(struct linux_binprm *bprm, + struct elfhdr *elf_ex, + struct elf_phdr *elf_phdata); +#else +static inline int register_elf_plugin(struct elf_plugin *plugin) +{ + return 0; +} +static inline void unregister_elf_plugin(struct elf_plugin *plugin) +{ +} +static inline struct file *elf_plugin_open_interpreter(struct linux_binprm *bprm, + struct elfhdr *elf_ex, + struct elf_phdr *elf_phdata) +{ + return NULL; +} +#endif + +#endif /* _LINUX_ELF_PLUGINS_H */ -- 2.51.2