All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
@ 2026-06-22  4:39 Farid Zakaria
  2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
                   ` (2 more replies)
  0 siblings, 3 replies; 29+ messages in thread
From: Farid Zakaria @ 2026-06-22  4:39 UTC (permalink / raw)
  To: kees, brauner, viro
  Cc: jack, shuah, linux-fsdevel, linux-mm, linux-kselftest,
	linux-kernel, Farid Zakaria

Currently, standard ELF and ELF FDPIC loaders require a fixed, absolute
path to the dynamic linker/interpreter (specified via PT_INTERP). This
creates significant inflexibility for relocatable dynamic interpreters,
where binaries are packaged independent of global system paths.

The primary goal of this patch series is to support relocatable binaries
in Nix, where packages are stored in a read-only store (typically /nix/store).
Allowing the ELF interpreter path to be resolved dynamically relative to
the binary's location via $ORIGIN enables Nix packages to be relocated
without needing patchelf or wrapper scripts.

For details on the design and motivation for this in Nix, see:
https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries

This series introduces support for resolving the $ORIGIN placeholder in 
the ELF interpreter path, bringing the kernel's binary loading behavior
in line with user-space dynamic linker origin resolution. 

To achieve this cleanly:
- We introduce a shared 'resolve_elf_interpreter()' helper in the VFS
  exec subsystem to avoid code duplication across loader implementations.
- For security, we restrict detection strictly to the prefix string
  "$ORIGIN" to prevent complex parsing exploits in kernel space.

Testing & Verification:
- Added a KUnit test case verifying path resolution logic.
- Added a kselftests integration test checking that a dynamically
  linked binary with its interpreter set to '$ORIGIN/mock_interp' successfully
  loads the mock interpreter (built statically using nolibc to avoid
  glibc TLS setup constraints during interpreter load-time).
- Verified end-to-end correct execution (PASS) using a minimal initramfs
  under QEMU.

Farid Zakaria (2):
  fs: support $ORIGIN in ELF interpreter paths
  selftests/exec: add test suites for $ORIGIN interpreter resolution

 fs/binfmt_elf.c                               | 11 ++++-
 fs/binfmt_elf_fdpic.c                         | 15 ++++++-
 fs/exec.c                                     | 42 +++++++++++++++++++
 fs/tests/exec_kunit.c                         | 26 ++++++++++++
 include/linux/binfmts.h                       |  2 +
 tools/testing/selftests/exec/Makefile         | 12 ++++--
 tools/testing/selftests/exec/mock_interp.c    |  6 +++
 tools/testing/selftests/exec/origin_interp.sh | 16 +++++++
 tools/testing/selftests/exec/test_prog.c      |  5 +++
 9 files changed, 128 insertions(+), 7 deletions(-)
 create mode 100644 tools/testing/selftests/exec/mock_interp.c
 create mode 100755 tools/testing/selftests/exec/origin_interp.sh
 create mode 100644 tools/testing/selftests/exec/test_prog.c

-- 
2.51.2


^ permalink raw reply	[flat|nested] 29+ messages in thread

* [PATCH 1/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22  4:39 [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Farid Zakaria
@ 2026-06-22  4:39 ` Farid Zakaria
  2026-06-22  9:53   ` Jori Koolstra
  2026-06-23 20:14   ` Kees Cook
  2026-06-22  4:39 ` [PATCH 2/2] selftests/exec: add test suites for $ORIGIN interpreter resolution Farid Zakaria
  2026-06-22 10:39 ` [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Jan Kara
  2 siblings, 2 replies; 29+ messages in thread
From: Farid Zakaria @ 2026-06-22  4:39 UTC (permalink / raw)
  To: kees, brauner, viro
  Cc: jack, shuah, linux-fsdevel, linux-mm, linux-kselftest,
	linux-kernel, Farid Zakaria

Currently, standard ELF and ELF FDPIC loaders expect a fixed path to the
dynamic linker/interpreter (PT_INTERP). However, for systems utilizing
relocatable dynamic interpreters (such as Nix/store-based environments),
hardcoding this path is inflexible and breaks binary portability.

Introduce support for resolving the $ORIGIN placeholder in the ELF
interpreter path. This maps the dynamic linker relative to the path
of the binary being executed, matching user-space origin resolution.

To avoid code duplication, implement a shared 'resolve_elf_interpreter()'
helper in the VFS exec layer. For safety, limit detection strictly to
the prefix string "$ORIGIN" to prevent complex parsing exploits.

Assisted-by: Antigravity:Gemini-Pro
Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---
 fs/binfmt_elf.c         | 11 +++++++++--
 fs/binfmt_elf_fdpic.c   | 15 +++++++++++++--
 fs/exec.c               | 42 +++++++++++++++++++++++++++++++++++++++++
 include/linux/binfmts.h |  2 ++
 4 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index 16a56b6b3..af11f96ae 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -872,7 +872,7 @@ static int load_elf_binary(struct linux_binprm *bprm)
 
 	elf_ppnt = elf_phdata;
 	for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
-		char *elf_interpreter;
+		char *elf_interpreter, *resolved_interp;
 
 		if (elf_ppnt->p_type == PT_GNU_PROPERTY) {
 			elf_property_phdata = elf_ppnt;
@@ -904,8 +904,15 @@ static int load_elf_binary(struct linux_binprm *bprm)
 		if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
 			goto out_free_interp;
 
-		interpreter = open_exec(elf_interpreter);
+		resolved_interp = resolve_elf_interpreter(bprm, elf_interpreter);
 		kfree(elf_interpreter);
+		if (IS_ERR(resolved_interp)) {
+			retval = PTR_ERR(resolved_interp);
+			goto out_free_ph;
+		}
+
+		interpreter = open_exec(resolved_interp);
+		kfree(resolved_interp);
 		retval = PTR_ERR(interpreter);
 		if (IS_ERR(interpreter))
 			goto out_free_ph;
diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c
index 7e3108489..e85727d71 100644
--- a/fs/binfmt_elf_fdpic.c
+++ b/fs/binfmt_elf_fdpic.c
@@ -230,7 +230,9 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)
 
 	for (i = 0; i < exec_params.hdr.e_phnum; i++, phdr++) {
 		switch (phdr->p_type) {
-		case PT_INTERP:
+		case PT_INTERP: {
+			char *resolved_interp;
+
 			retval = -ENOMEM;
 			if (phdr->p_filesz > PATH_MAX)
 				goto error;
@@ -259,7 +261,15 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)
 			kdebug("Using ELF interpreter %s", interpreter_name);
 
 			/* replace the program with the interpreter */
-			interpreter = open_exec(interpreter_name);
+			resolved_interp = resolve_elf_interpreter(bprm, interpreter_name);
+			kfree(interpreter_name);
+			if (IS_ERR(resolved_interp)) {
+				retval = PTR_ERR(resolved_interp);
+				goto error;
+			}
+
+			interpreter = open_exec(resolved_interp);
+			kfree(resolved_interp);
 			retval = PTR_ERR(interpreter);
 			if (IS_ERR(interpreter)) {
 				interpreter = NULL;
@@ -284,6 +294,7 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)
 
 			interp_params.hdr = *((struct elfhdr *) bprm->buf);
 			break;
+		}
 
 		case PT_LOAD:
 #ifdef CONFIG_MMU
diff --git a/fs/exec.c b/fs/exec.c
index b92fe7db1..0978ae613 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -2024,6 +2024,48 @@ static int __init init_fs_exec_sysctls(void)
 fs_initcall(init_fs_exec_sysctls);
 #endif /* CONFIG_SYSCTL */
 
+char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter)
+{
+	char *pathbuf, *path, *slash, *resolved;
+
+	if (strncmp(elf_interpreter, "$ORIGIN", 7) != 0) {
+		char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
+
+		return ret ? ret : ERR_PTR(-ENOMEM);
+	}
+
+	pathbuf = kmalloc(PATH_MAX, GFP_KERNEL);
+	if (!pathbuf)
+		return ERR_PTR(-ENOMEM);
+
+	path = file_path(bprm->file, pathbuf, PATH_MAX);
+	if (IS_ERR(path)) {
+		kfree(pathbuf);
+		return (char *)path;
+	}
+
+	slash = strrchr(path, '/');
+	if (slash) {
+		if (slash == path)
+			*(slash + 1) = '\0';
+		else
+			*slash = '\0';
+	} else {
+		kfree(pathbuf);
+		char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
+
+		return ret ? ret : ERR_PTR(-ENOMEM);
+	}
+
+	resolved = kasprintf(GFP_KERNEL, "%s%s", path, elf_interpreter + 7);
+	kfree(pathbuf);
+	if (!resolved)
+		return ERR_PTR(-ENOMEM);
+
+	return resolved;
+}
+EXPORT_SYMBOL(resolve_elf_interpreter);
+
 #ifdef CONFIG_EXEC_KUNIT_TEST
 #include "tests/exec_kunit.c"
 #endif
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index 2c77e383e..17419cd3d 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -150,4 +150,6 @@ extern ssize_t read_code(struct file *, unsigned long, loff_t, size_t);
 int kernel_execve(const char *filename,
 		  const char *const *argv, const char *const *envp);
 
+char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter);
+
 #endif /* _LINUX_BINFMTS_H */
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH 2/2] selftests/exec: add test suites for $ORIGIN interpreter resolution
  2026-06-22  4:39 [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Farid Zakaria
  2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
@ 2026-06-22  4:39 ` Farid Zakaria
  2026-06-22 10:39 ` [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Jan Kara
  2 siblings, 0 replies; 29+ messages in thread
From: Farid Zakaria @ 2026-06-22  4:39 UTC (permalink / raw)
  To: kees, brauner, viro
  Cc: jack, shuah, linux-fsdevel, linux-mm, linux-kselftest,
	linux-kernel, Farid Zakaria

Add verification suites to test the kernel VFS and ELF loader $ORIGIN
interpreter resolution.

1. Add a KUnit unit test 'exec_test_resolve_elf_interpreter()' verifying
   path resolution format logic.
2. Add a kselftests integration test containing:
   - A nolibc-based statically linked mock interpreter that prints a
     success message and returns 42. nolibc is used to bypass glibc's
     static startup code which segfaults when loaded as an interpreter
     due to AT_PHDR mismatches.
   - A dynamic test program configured to look for the interpreter at
     $ORIGIN/mock_interp.
   - A shell script harness checking for a PASS result.

Assisted-by: Antigravity:Gemini-Pro
Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---
 fs/tests/exec_kunit.c                         | 26 +++++++++++++++++++
 tools/testing/selftests/exec/Makefile         | 12 ++++++---
 tools/testing/selftests/exec/mock_interp.c    |  6 +++++
 tools/testing/selftests/exec/origin_interp.sh | 16 ++++++++++++
 tools/testing/selftests/exec/test_prog.c      |  5 ++++
 5 files changed, 62 insertions(+), 3 deletions(-)
 create mode 100644 tools/testing/selftests/exec/mock_interp.c
 create mode 100755 tools/testing/selftests/exec/origin_interp.sh
 create mode 100644 tools/testing/selftests/exec/test_prog.c

diff --git a/fs/tests/exec_kunit.c b/fs/tests/exec_kunit.c
index 1c32cac09..991b9abad 100644
--- a/fs/tests/exec_kunit.c
+++ b/fs/tests/exec_kunit.c
@@ -119,8 +119,34 @@ static void exec_test_bprm_stack_limits(struct kunit *test)
 	}
 }
 
+static void exec_test_resolve_elf_interpreter(struct kunit *test)
+{
+	struct linux_binprm bprm = { .file = NULL };
+	struct file *f;
+	char *resolved;
+
+	// Test 1: Non-$ORIGIN interpreter path should just be duplicated
+	resolved = resolve_elf_interpreter(&bprm, "/lib64/ld-linux-x86-64.so.2");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, resolved);
+	KUNIT_EXPECT_STREQ(test, resolved, "/lib64/ld-linux-x86-64.so.2");
+	kfree(resolved);
+
+	// Test 2: $ORIGIN interpreter path
+	f = filp_open("/", O_RDONLY, 0);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, f);
+	bprm.file = f;
+
+	resolved = resolve_elf_interpreter(&bprm, "$ORIGIN/../lib/ld.so");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, resolved);
+	KUNIT_EXPECT_STREQ(test, resolved, "//../lib/ld.so");
+	kfree(resolved);
+
+	filp_close(f, NULL);
+}
+
 static struct kunit_case exec_test_cases[] = {
 	KUNIT_CASE(exec_test_bprm_stack_limits),
+	KUNIT_CASE(exec_test_resolve_elf_interpreter),
 	{},
 };
 
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 45a3cfc43..5e2e305cb 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -10,9 +10,9 @@ ALIGN_PIES        := $(patsubst %,load_address.%,$(ALIGNS))
 ALIGN_STATIC_PIES := $(patsubst %,load_address.static.%,$(ALIGNS))
 ALIGNMENT_TESTS   := $(ALIGN_PIES) $(ALIGN_STATIC_PIES)
 
-TEST_PROGS := binfmt_script.py check-exec-tests.sh
-TEST_GEN_PROGS := execveat non-regular $(ALIGNMENT_TESTS)
-TEST_GEN_PROGS_EXTENDED := false inc set-exec script-exec.inc script-noexec.inc
+TEST_PROGS := binfmt_script.py check-exec-tests.sh origin_interp.sh
+TEST_GEN_PROGS := execveat non-regular $(ALIGNMENT_TESTS) test_prog
+TEST_GEN_PROGS_EXTENDED := false inc set-exec script-exec.inc script-noexec.inc mock_interp
 TEST_GEN_FILES := execveat.symlink execveat.denatured script subdir
 # Makefile is a run-time dependency, since it's accessed by the execveat test
 TEST_FILES := Makefile
@@ -55,3 +55,9 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
 	cp $< $@
 $(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
 	cp $< $@
+
+$(OUTPUT)/mock_interp: mock_interp.c
+	$(CC) $(CFLAGS) $(LDFLAGS) -static -nostdlib -include ../../../include/nolibc/nolibc.h $< -o $@
+
+$(OUTPUT)/test_prog: test_prog.c $(OUTPUT)/mock_interp
+	$(CC) $(CFLAGS) $(LDFLAGS) -Wl,-dynamic-linker,'$$ORIGIN/mock_interp' $< -o $@
diff --git a/tools/testing/selftests/exec/mock_interp.c b/tools/testing/selftests/exec/mock_interp.c
new file mode 100644
index 000000000..9c9ca1098
--- /dev/null
+++ b/tools/testing/selftests/exec/mock_interp.c
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: GPL-2.0
+int main(void)
+{
+	write(1, "Hello from mock interpreter!\n", 29);
+	return 42;
+}
diff --git a/tools/testing/selftests/exec/origin_interp.sh b/tools/testing/selftests/exec/origin_interp.sh
new file mode 100755
index 000000000..635a40839
--- /dev/null
+++ b/tools/testing/selftests/exec/origin_interp.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+
+# Execute the test program which has its interpreter set to $ORIGIN/mock_interp
+# Note that mock_interp must be in the same directory.
+dir=$(dirname "$0")
+out=$("$dir"/test_prog 2>&1)
+exit_code=$?
+
+if [ $exit_code -eq 42 ] && [ "$out" = "Hello from mock interpreter!" ]; then
+	echo "origin_interp: PASS"
+	exit 0
+else
+	echo "origin_interp: FAIL (exit_code=$exit_code, output='$out')"
+	exit 1
+fi
diff --git a/tools/testing/selftests/exec/test_prog.c b/tools/testing/selftests/exec/test_prog.c
new file mode 100644
index 000000000..451614def
--- /dev/null
+++ b/tools/testing/selftests/exec/test_prog.c
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0
+int main(void)
+{
+	return 0; /* Should never be reached if interpreter is loaded instead */
+}
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* Re: [PATCH 1/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
@ 2026-06-22  9:53   ` Jori Koolstra
  2026-06-23 20:14   ` Kees Cook
  1 sibling, 0 replies; 29+ messages in thread
From: Jori Koolstra @ 2026-06-22  9:53 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: kees, brauner, viro, jack, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, linux-kernel

Hi Farid,

On Sun, Jun 21, 2026 at 09:39:33PM -0700, Farid Zakaria wrote:
> Currently, standard ELF and ELF FDPIC loaders expect a fixed path to the
> dynamic linker/interpreter (PT_INTERP). However, for systems utilizing
> relocatable dynamic interpreters (such as Nix/store-based environments),
> hardcoding this path is inflexible and breaks binary portability.
> 
> Introduce support for resolving the $ORIGIN placeholder in the ELF
> interpreter path. This maps the dynamic linker relative to the path
> of the binary being executed, matching user-space origin resolution.
> 
> To avoid code duplication, implement a shared 'resolve_elf_interpreter()'
> helper in the VFS exec layer. For safety, limit detection strictly to
> the prefix string "$ORIGIN" to prevent complex parsing exploits.
> 
> Assisted-by: Antigravity:Gemini-Pro

This isn't a requirement from the community or anything, but I always
find it useful if I see an Assisted-by tag to know what assistence was
actually delivered by an LLM. Otherwise we might as well add
assisted-by tags for any editor.

Talking about LLMs, your patch has some issues flagged by Sashiko[1].
Please take a look.


> Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
> ---
>  fs/binfmt_elf.c         | 11 +++++++++--
>  fs/binfmt_elf_fdpic.c   | 15 +++++++++++++--
>  fs/exec.c               | 42 +++++++++++++++++++++++++++++++++++++++++
>  include/linux/binfmts.h |  2 ++
>  4 files changed, 66 insertions(+), 4 deletions(-)
> 
> diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
> index 16a56b6b3..af11f96ae 100644
> --- a/fs/binfmt_elf.c
> +++ b/fs/binfmt_elf.c
> @@ -872,7 +872,7 @@ static int load_elf_binary(struct linux_binprm *bprm)
>  
>  	elf_ppnt = elf_phdata;
>  	for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
> -		char *elf_interpreter;
> +		char *elf_interpreter, *resolved_interp;
>  
>  		if (elf_ppnt->p_type == PT_GNU_PROPERTY) {
>  			elf_property_phdata = elf_ppnt;
> @@ -904,8 +904,15 @@ static int load_elf_binary(struct linux_binprm *bprm)
>  		if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
>  			goto out_free_interp;
>  
> -		interpreter = open_exec(elf_interpreter);
> +		resolved_interp = resolve_elf_interpreter(bprm, elf_interpreter);
>  		kfree(elf_interpreter);
> +		if (IS_ERR(resolved_interp)) {
> +			retval = PTR_ERR(resolved_interp);
> +			goto out_free_ph;
> +		}
> +
> +		interpreter = open_exec(resolved_interp);
> +		kfree(resolved_interp);
>  		retval = PTR_ERR(interpreter);
>  		if (IS_ERR(interpreter))
>  			goto out_free_ph;
> diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c
> index 7e3108489..e85727d71 100644
> --- a/fs/binfmt_elf_fdpic.c
> +++ b/fs/binfmt_elf_fdpic.c
> @@ -230,7 +230,9 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)
>  
>  	for (i = 0; i < exec_params.hdr.e_phnum; i++, phdr++) {
>  		switch (phdr->p_type) {
> -		case PT_INTERP:
> +		case PT_INTERP: {
> +			char *resolved_interp;
> +
>  			retval = -ENOMEM;
>  			if (phdr->p_filesz > PATH_MAX)
>  				goto error;
> @@ -259,7 +261,15 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)
>  			kdebug("Using ELF interpreter %s", interpreter_name);
>  
>  			/* replace the program with the interpreter */
> -			interpreter = open_exec(interpreter_name);
> +			resolved_interp = resolve_elf_interpreter(bprm, interpreter_name);
> +			kfree(interpreter_name);
> +			if (IS_ERR(resolved_interp)) {
> +				retval = PTR_ERR(resolved_interp);
> +				goto error;
> +			}
> +
> +			interpreter = open_exec(resolved_interp);
> +			kfree(resolved_interp);
>  			retval = PTR_ERR(interpreter);
>  			if (IS_ERR(interpreter)) {
>  				interpreter = NULL;
> @@ -284,6 +294,7 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)
>  
>  			interp_params.hdr = *((struct elfhdr *) bprm->buf);
>  			break;
> +		}
>  
>  		case PT_LOAD:
>  #ifdef CONFIG_MMU
> diff --git a/fs/exec.c b/fs/exec.c
> index b92fe7db1..0978ae613 100644
> --- a/fs/exec.c
> +++ b/fs/exec.c
> @@ -2024,6 +2024,48 @@ static int __init init_fs_exec_sysctls(void)
>  fs_initcall(init_fs_exec_sysctls);
>  #endif /* CONFIG_SYSCTL */
>  
> +char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter)
> +{
> +	char *pathbuf, *path, *slash, *resolved;
> +
> +	if (strncmp(elf_interpreter, "$ORIGIN", 7) != 0) {
> +		char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
> +
> +		return ret ? ret : ERR_PTR(-ENOMEM);
> +	}
> +
> +	pathbuf = kmalloc(PATH_MAX, GFP_KERNEL);
> +	if (!pathbuf)
> +		return ERR_PTR(-ENOMEM);
> +
> +	path = file_path(bprm->file, pathbuf, PATH_MAX);
> +	if (IS_ERR(path)) {
> +		kfree(pathbuf);
> +		return (char *)path;
> +	}
> +
> +	slash = strrchr(path, '/');
> +	if (slash) {
> +		if (slash == path)
> +			*(slash + 1) = '\0';
> +		else
> +			*slash = '\0';
> +	} else {
> +		kfree(pathbuf);
> +		char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
> +
> +		return ret ? ret : ERR_PTR(-ENOMEM);
> +	}
> +
> +	resolved = kasprintf(GFP_KERNEL, "%s%s", path, elf_interpreter + 7);
> +	kfree(pathbuf);
> +	if (!resolved)
> +		return ERR_PTR(-ENOMEM);
> +
> +	return resolved;
> +}
> +EXPORT_SYMBOL(resolve_elf_interpreter);
> +
>  #ifdef CONFIG_EXEC_KUNIT_TEST
>  #include "tests/exec_kunit.c"
>  #endif
> diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
> index 2c77e383e..17419cd3d 100644
> --- a/include/linux/binfmts.h
> +++ b/include/linux/binfmts.h
> @@ -150,4 +150,6 @@ extern ssize_t read_code(struct file *, unsigned long, loff_t, size_t);
>  int kernel_execve(const char *filename,
>  		  const char *const *argv, const char *const *envp);
>  
> +char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter);
> +
>  #endif /* _LINUX_BINFMTS_H */
> -- 
> 2.51.2
> 

Thanks,
Jori.

[1]: https://sashiko.dev/#/patchset/20260622043934.179879-1-farid.m.zakaria%40gmail.com


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22  4:39 [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Farid Zakaria
  2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
  2026-06-22  4:39 ` [PATCH 2/2] selftests/exec: add test suites for $ORIGIN interpreter resolution Farid Zakaria
@ 2026-06-22 10:39 ` Jan Kara
  2026-06-22 17:15   ` Farid Zakaria
  2 siblings, 1 reply; 29+ messages in thread
From: Jan Kara @ 2026-06-22 10:39 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: kees, brauner, viro, jack, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, linux-kernel

On Sun 21-06-26 21:39:32, Farid Zakaria wrote:
> Currently, standard ELF and ELF FDPIC loaders require a fixed, absolute
> path to the dynamic linker/interpreter (specified via PT_INTERP). This
> creates significant inflexibility for relocatable dynamic interpreters,
> where binaries are packaged independent of global system paths.
> 
> The primary goal of this patch series is to support relocatable binaries
> in Nix, where packages are stored in a read-only store (typically /nix/store).
> Allowing the ELF interpreter path to be resolved dynamically relative to
> the binary's location via $ORIGIN enables Nix packages to be relocated
> without needing patchelf or wrapper scripts.
> 
> For details on the design and motivation for this in Nix, see:
> https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries
> 
> This series introduces support for resolving the $ORIGIN placeholder in 
> the ELF interpreter path, bringing the kernel's binary loading behavior
> in line with user-space dynamic linker origin resolution. 

Thanks for the patches! Before dwelving into implementation details we
need to discuss whether something like this even belongs to the kernel.
Frankly to me this looks rather arbitrary and tied to the particular way
you've decided to setup your package management system. In particular the
usual answer for situations like these is to use namespaces which you
discount in your blog with: "If you are using tools like Bazel or Buck2
they likely already employ their own sandboxing via namespacing for builds.
Integrating Nix into these ecosystems becomes incredibly impractical
because we run into nested user namespace and mount restrictions."

I don't know enough details to really judge but it seems to me like you are
trying to workaround a mess in userspace with kernel changes.

Anyway I'm pretty sure Christian will have more educated answer than me but
I just wanted to express my skepticism about this approach and that perhaps
you wait for feedback from him before spending more time on these patches.

								Honza
-- 
Jan Kara <jack@suse.com>
SUSE Labs, CR


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22 10:39 ` [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Jan Kara
@ 2026-06-22 17:15   ` Farid Zakaria
  2026-06-22 21:08     ` John Ericson
  0 siblings, 1 reply; 29+ messages in thread
From: Farid Zakaria @ 2026-06-22 17:15 UTC (permalink / raw)
  To: Jan Kara
  Cc: kees, brauner, viro, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, linux-kernel

On Mon, Jun 22, 2026 at 3:40 AM Jan Kara <jack@suse.cz> wrote:
>
> On Sun 21-06-26 21:39:32, Farid Zakaria wrote:
> > Currently, standard ELF and ELF FDPIC loaders require a fixed, absolute
> > path to the dynamic linker/interpreter (specified via PT_INTERP). This
> > creates significant inflexibility for relocatable dynamic interpreters,
> > where binaries are packaged independent of global system paths.
> >
> > The primary goal of this patch series is to support relocatable binaries
> > in Nix, where packages are stored in a read-only store (typically /nix/store).
> > Allowing the ELF interpreter path to be resolved dynamically relative to
> > the binary's location via $ORIGIN enables Nix packages to be relocated
> > without needing patchelf or wrapper scripts.
> >
> > For details on the design and motivation for this in Nix, see:
> > https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries
> >
> > This series introduces support for resolving the $ORIGIN placeholder in
> > the ELF interpreter path, bringing the kernel's binary loading behavior
> > in line with user-space dynamic linker origin resolution.
>
> Thanks for the patches! Before dwelving into implementation details we
> need to discuss whether something like this even belongs to the kernel.
> Frankly to me this looks rather arbitrary and tied to the particular way
> you've decided to setup your package management system. In particular the
> usual answer for situations like these is to use namespaces which you
> discount in your blog with: "If you are using tools like Bazel or Buck2
> they likely already employ their own sandboxing via namespacing for builds.
> Integrating Nix into these ecosystems becomes incredibly impractical
> because we run into nested user namespace and mount restrictions."
>
> I don't know enough details to really judge but it seems to me like you are
> trying to workaround a mess in userspace with kernel changes.
>
> Anyway I'm pretty sure Christian will have more educated answer than me but
> I just wanted to express my skepticism about this approach and that perhaps
> you wait for feedback from him before spending more time on these patches.
>
>                                                                 Honza
> --
> Jan Kara <jack@suse.com>
> SUSE Labs, CR

Thank you for taking the time to look at my patch.
I'm new to contributing to Linux mailing list so receiving feedback
and responses is welcoming :)

Having put forward the patch, I'm clearly biased toward thinking this
support should exist in the kernel.
If I had to think to strengthen my argument would be that the kernel
should not be imposing how the interpreter is found on userland.
Finding the interpreter relative to the binary would be useful for
package deployment scenarios similar to app-bundles beyond systems
like Nix -- which is the originating reason why $ORIGIN exists in the
dynamic linker.

To me, the gap is that prior to systems like Nix, the idea of wanting
your dynamic linker to be part of your app bundle was not necessary
but Nix models the dependency chain down to the loader. Such
functionality would be even more correct for these other bundled
solutions as well, making them portable across userspace glibc
versions for instance.

I see Jori Koolstra mentioned that Sashiko found feedback on the
implementation.
Is it worthwhile to amend the patch now or should I wait to hear back
on whether such a contribution would be accepted?

Jori:
I'm not 100% clear on your question but the LLM assisted with some
code generation and brainstorming.

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22 17:15   ` Farid Zakaria
@ 2026-06-22 21:08     ` John Ericson
  2026-06-25  8:50       ` Christian Brauner
  0 siblings, 1 reply; 29+ messages in thread
From: John Ericson @ 2026-06-22 21:08 UTC (permalink / raw)
  To: Farid Zakaria, Jan Kara
  Cc: Kees Cook, Christian Brauner, Al Viro, shuah, linux-fsdevel,
	linux-mm, linux-kselftest, LKML

Hi, I am another Nix developer, and have participated in some LKML
discussions in the (recent and distant) past, and thought I should weigh
in here too.

On Mon, Jun 22, 2026, at 1:15 PM, Farid Zakaria wrote:
> On Mon, Jun 22, 2026 at 3:40 AM Jan Kara <jack@suse.cz> wrote:
> > Thanks for the patches! Before dwelving into implementation details we
> > need to discuss whether something like this even belongs to the kernel.
> > Frankly to me this looks rather arbitrary and tied to the particular way
> > you've decided to setup your package management system.
> >
> > I don't know enough details to really judge but it seems to me like you are
> > trying to workaround a mess in userspace with kernel changes.
>
> Having put forward the patch, I'm clearly biased toward thinking this
> support should exist in the kernel.
> If I had to think to strengthen my argument would be that the kernel
> should not be imposing how the interpreter is found on userland.
> Finding the interpreter relative to the binary would be useful for
> package deployment scenarios similar to app-bundles beyond systems
> like Nix -- which is the originating reason why $ORIGIN exists in the
> dynamic linker.

Yes, the idea of making "relocatable software" is not a new one, and
indeed it is why `$ORIGIN` is supported in the RPATH etc. in the first
place.

Most of the programming model for writing relocatable software is fixed
at this point. For example, /proc/self/exe made it much easier to look
up arbitrary stuff relevant to the current executable. It is just some
initial entry point stuff (the ELF interpreter, and shebangs) which is a
glaring exception. Those should support `$ORIGIN` too. There is no good
technical justification (that I can think of) for some but not all of
these supporting `$ORIGIN` --- either it makes sense everywhere, or it
makes sense nowhere.

(I suspect the only reason it didn't happen was pure inertia/Conway's
law --- easier for whoever was excited about `$ORIGIN` to change the
glibc loader than the kernel.)

> To me, the gap is that prior to systems like Nix, the idea of wanting
> your dynamic linker to be part of your app bundle was not necessary
> but Nix models the dependency chain down to the loader. Such
> functionality would be even more correct for these other bundled
> solutions as well, making them portable across userspace glibc
> versions for instance.

Yes, exactly. Traditionally people thought "eh `/lib/ld-linux.so.*`
doesn't change too much", and decided relocatable software that
nonetheless hard-coded that absolute path to an unknown system-provided
ELF interpreter was good enough. (Or if they weren't good enough, they
went with static linking, but that imposes other costs.)

Now there do exist purely-user-space work-arounds, like
https://github.com/Mic92/wrap-buddy, but they are quite complex, and
involve various patching trickery that is likely to scare a lot of
security analysis tools. A kernel-based solution that allows clean
declarative expression of intent with `$ORIGIN` is much more elegant.

> > In particular the
> > usual answer for situations like these is to use namespaces which you
> > discount in your blog with: "If you are using tools like Bazel or Buck2
> > they likely already employ their own sandboxing via namespacing for builds.
> > Integrating Nix into these ecosystems becomes incredibly impractical
> > because we run into nested user namespace and mount restrictions."

I think it is good to see what Conda does as documented in
<https://docs.conda.io/projects/conda-build/en/stable/resources/make-relocatable.html>
and consider why relying on namespaces vs good old-fashioned relocatable
isn't good enough for them either.

(I don't doubt that Conda would find this approach more robust than
their sedding tricks, and prefer to use it where possible.)

The short answer is while all of us in the build system space love
sandboxing during the build, we don't want that to lead to *requiring*
run time sandboxing of the built artifacts. For example, we can
certainly arrange sandboxing so `/lib/ld-linux.so.*` is the one that
some executable expects now, but every time that executable is run, it
*must* be run in a root filesystem where `/lib/ld-linux.so.*` is the
loader it expects.

If you have multiple programs that (for whatever reasons) expect
multiple different loaders, all spawning one another, it would
potentially incur quite the development cost to ensure that they all do
the proper unsharing to make everything work.

Relocatability recognizes that whether or not namespaces exist, in an
"open world" scenario where we don't know how the software we are
writing will be combined with other software for deployment downstream
in different ways, it is easiest to adopt an idiom where different
things can be placed at different absolute paths, at the user's
discretion, and so conflicts are always avoidable.

> > Anyway I'm pretty sure Christian will have more educated answer than me but
> > I just wanted to express my skepticism about this approach and that perhaps
> > you wait for feedback from him before spending more time on these patches.
> >
> >                                                                 Honza
> > --
> > Jan Kara <jack@suse.com>
> > SUSE Labs, CR

Waiting makes sense, I am curious too what he will have to say.

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 1/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
  2026-06-22  9:53   ` Jori Koolstra
@ 2026-06-23 20:14   ` Kees Cook
  2026-06-23 20:35     ` Farid Zakaria
  1 sibling, 1 reply; 29+ messages in thread
From: Kees Cook @ 2026-06-23 20:14 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: brauner, viro, jack, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, linux-kernel

On Sun, Jun 21, 2026 at 09:39:33PM -0700, Farid Zakaria wrote:
> Currently, standard ELF and ELF FDPIC loaders expect a fixed path to the
> dynamic linker/interpreter (PT_INTERP). However, for systems utilizing
> relocatable dynamic interpreters (such as Nix/store-based environments),
> hardcoding this path is inflexible and breaks binary portability.
> 
> Introduce support for resolving the $ORIGIN placeholder in the ELF
> interpreter path. This maps the dynamic linker relative to the path
> of the binary being executed, matching user-space origin resolution.
> 
> To avoid code duplication, implement a shared 'resolve_elf_interpreter()'
> helper in the VFS exec layer. For safety, limit detection strictly to
> the prefix string "$ORIGIN" to prevent complex parsing exploits.

Does any other OS that implements ELF support also support $ORIGIN in
the loader? $ORIGIN isn't even part of the ELF spec at all and is a
glibc extension, IIUC.

I'm not excited about path-based string manipulations as they end up
being racy, and mucking with loader path seems like we're inviting
trouble (since the _binary_ specifies setuid-ness), and we've seen
issues with $ORIGIN before, even strictly outside of the kernel:
https://seclists.org/fulldisclosure/2010/Oct/257

> [...]
> diff --git a/fs/exec.c b/fs/exec.c
> index b92fe7db1..0978ae613 100644
> --- a/fs/exec.c
> +++ b/fs/exec.c
> @@ -2024,6 +2024,48 @@ static int __init init_fs_exec_sysctls(void)
>  fs_initcall(init_fs_exec_sysctls);
>  #endif /* CONFIG_SYSCTL */
>  
> +char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter)
> +{
> +	char *pathbuf, *path, *slash, *resolved;
> +
> +	if (strncmp(elf_interpreter, "$ORIGIN", 7) != 0) {
> +		char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
> +
> +		return ret ? ret : ERR_PTR(-ENOMEM);
> +	}

But even if we did take this, I really don't want to incur a universal
penalty on exec for it. This is doing a kmalloc+dup (and later kfree)
for all non-$ORIGIN execs. So even if this gets added, it needs to be
handled differently.

I would probably say this helper should return a struct file * instead
and have a fast-path for the common case. I think a check for leading
'$' (if strncmp doesn't get inlined) would be okay here as far as
"incurring common performance cost"; that string is almost certainly
cache-hot.

> +	pathbuf = kmalloc(PATH_MAX, GFP_KERNEL);
> +	if (!pathbuf)
> +		return ERR_PTR(-ENOMEM);
> +
> +	path = file_path(bprm->file, pathbuf, PATH_MAX);
> +	if (IS_ERR(path)) {
> +		kfree(pathbuf);
> +		return (char *)path;
> +	}

This still just _feels_ like an info leak or a race condition to me. I
can't give a credible example, though. But it creeps me out. :)
(I note the blog post also says "and the shabang" and I get even more
creeped out about seeing that patch.)

> +
> +	slash = strrchr(path, '/');
> +	if (slash) {
> +		if (slash == path)
> +			*(slash + 1) = '\0';

This is not idiomatic string manipulation.

> +		else
> +			*slash = '\0';

More readable, IMO, as:

	if (slash)
		slash[1] = '\0';
	else
		path = "";

But does this match the glibc resolution logic? i.e. should it be:

	if (strncmp(elf_interpreter, "$ORIGIN/", 8) != 0)
	...
	if (!slash)
		slash = path;
	*slash = '\0';
	...
	resolved = kasprintf(GFP_KERNEL, "%s/%s", path, elf_interpreter + 8);

(requires the trailing /)

> +	} else {
> +		kfree(pathbuf);
> +		char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
> +
> +		return ret ? ret : ERR_PTR(-ENOMEM);

This is the same as the logic top of the function. This repetition smells
of the LLM. :)

> +	}
> +
> +	resolved = kasprintf(GFP_KERNEL, "%s%s", path, elf_interpreter + 7);
> +	kfree(pathbuf);
> +	if (!resolved)
> +		return ERR_PTR(-ENOMEM);
> +
> +	return resolved;
> +}
> +EXPORT_SYMBOL(resolve_elf_interpreter);
> +
>  #ifdef CONFIG_EXEC_KUNIT_TEST
>  #include "tests/exec_kunit.c"
>  #endif
> diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
> index 2c77e383e..17419cd3d 100644
> --- a/include/linux/binfmts.h
> +++ b/include/linux/binfmts.h
> @@ -150,4 +150,6 @@ extern ssize_t read_code(struct file *, unsigned long, loff_t, size_t);
>  int kernel_execve(const char *filename,
>  		  const char *const *argv, const char *const *envp);
>  
> +char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter);
> +
>  #endif /* _LINUX_BINFMTS_H */
> -- 
> 2.51.2
> 

So, I guess, I'd like more convincing, but I'm very happy to see a KUnit
test included!

-Kees

-- 
Kees Cook


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 1/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-23 20:14   ` Kees Cook
@ 2026-06-23 20:35     ` Farid Zakaria
  0 siblings, 0 replies; 29+ messages in thread
From: Farid Zakaria @ 2026-06-23 20:35 UTC (permalink / raw)
  To: Kees Cook
  Cc: brauner, viro, jack, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, linux-kernel

Thanks for all the improvement suggestions.
Yes, I leveraged an LLM to generate the initial code (open & honest)
but I'm willing to keep refining it to whatever state upstream
requires.
Thank you for not dismissing it outright right away.
(FWIW, the initial code was even worse so this is the product of me
intervening and editing it, despite my lack of C expertise).

A few more attempts at convincing :)
* musl also supports $ORIGIN
[https://elixir.bootlin.com/musl/v1.2.5/source/ldso/dynlink.c#L911] so
there seems to be strong convergence on the concept beyond glibc
* ideologically, everything about a program should be portable easily
from one environment to another (the goal of systems such as Nix).
Userland has allowed this support in nearly ever-space and the
DT_INTERP / shebang path from the kernel seems to be a missing gap.

We also have the shebang patch ready (CC @alan.urman@gmail.com ) but
we wanted to see the reception to this first.

I will wait to update the patch based on your feedback and Sashiko's,
if you are convinced just to avoid spamming us with more patch files
:)

Farid Zakaria

On Tue, Jun 23, 2026 at 1:14 PM Kees Cook <kees@kernel.org> wrote:
>
> On Sun, Jun 21, 2026 at 09:39:33PM -0700, Farid Zakaria wrote:
> > Currently, standard ELF and ELF FDPIC loaders expect a fixed path to the
> > dynamic linker/interpreter (PT_INTERP). However, for systems utilizing
> > relocatable dynamic interpreters (such as Nix/store-based environments),
> > hardcoding this path is inflexible and breaks binary portability.
> >
> > Introduce support for resolving the $ORIGIN placeholder in the ELF
> > interpreter path. This maps the dynamic linker relative to the path
> > of the binary being executed, matching user-space origin resolution.
> >
> > To avoid code duplication, implement a shared 'resolve_elf_interpreter()'
> > helper in the VFS exec layer. For safety, limit detection strictly to
> > the prefix string "$ORIGIN" to prevent complex parsing exploits.
>
> Does any other OS that implements ELF support also support $ORIGIN in
> the loader? $ORIGIN isn't even part of the ELF spec at all and is a
> glibc extension, IIUC.
>
> I'm not excited about path-based string manipulations as they end up
> being racy, and mucking with loader path seems like we're inviting
> trouble (since the _binary_ specifies setuid-ness), and we've seen
> issues with $ORIGIN before, even strictly outside of the kernel:
> https://seclists.org/fulldisclosure/2010/Oct/257
>
> > [...]
> > diff --git a/fs/exec.c b/fs/exec.c
> > index b92fe7db1..0978ae613 100644
> > --- a/fs/exec.c
> > +++ b/fs/exec.c
> > @@ -2024,6 +2024,48 @@ static int __init init_fs_exec_sysctls(void)
> >  fs_initcall(init_fs_exec_sysctls);
> >  #endif /* CONFIG_SYSCTL */
> >
> > +char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter)
> > +{
> > +     char *pathbuf, *path, *slash, *resolved;
> > +
> > +     if (strncmp(elf_interpreter, "$ORIGIN", 7) != 0) {
> > +             char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
> > +
> > +             return ret ? ret : ERR_PTR(-ENOMEM);
> > +     }
>
> But even if we did take this, I really don't want to incur a universal
> penalty on exec for it. This is doing a kmalloc+dup (and later kfree)
> for all non-$ORIGIN execs. So even if this gets added, it needs to be
> handled differently.
>
> I would probably say this helper should return a struct file * instead
> and have a fast-path for the common case. I think a check for leading
> '$' (if strncmp doesn't get inlined) would be okay here as far as
> "incurring common performance cost"; that string is almost certainly
> cache-hot.
>
> > +     pathbuf = kmalloc(PATH_MAX, GFP_KERNEL);
> > +     if (!pathbuf)
> > +             return ERR_PTR(-ENOMEM);
> > +
> > +     path = file_path(bprm->file, pathbuf, PATH_MAX);
> > +     if (IS_ERR(path)) {
> > +             kfree(pathbuf);
> > +             return (char *)path;
> > +     }
>
> This still just _feels_ like an info leak or a race condition to me. I
> can't give a credible example, though. But it creeps me out. :)
> (I note the blog post also says "and the shabang" and I get even more
> creeped out about seeing that patch.)
>
> > +
> > +     slash = strrchr(path, '/');
> > +     if (slash) {
> > +             if (slash == path)
> > +                     *(slash + 1) = '\0';
>
> This is not idiomatic string manipulation.
>
> > +             else
> > +                     *slash = '\0';
>
> More readable, IMO, as:
>
>         if (slash)
>                 slash[1] = '\0';
>         else
>                 path = "";
>
> But does this match the glibc resolution logic? i.e. should it be:
>
>         if (strncmp(elf_interpreter, "$ORIGIN/", 8) != 0)
>         ...
>         if (!slash)
>                 slash = path;
>         *slash = '\0';
>         ...
>         resolved = kasprintf(GFP_KERNEL, "%s/%s", path, elf_interpreter + 8);
>
> (requires the trailing /)
>
> > +     } else {
> > +             kfree(pathbuf);
> > +             char *ret = kstrdup(elf_interpreter, GFP_KERNEL);
> > +
> > +             return ret ? ret : ERR_PTR(-ENOMEM);
>
> This is the same as the logic top of the function. This repetition smells
> of the LLM. :)
>
> > +     }
> > +
> > +     resolved = kasprintf(GFP_KERNEL, "%s%s", path, elf_interpreter + 7);
> > +     kfree(pathbuf);
> > +     if (!resolved)
> > +             return ERR_PTR(-ENOMEM);
> > +
> > +     return resolved;
> > +}
> > +EXPORT_SYMBOL(resolve_elf_interpreter);
> > +
> >  #ifdef CONFIG_EXEC_KUNIT_TEST
> >  #include "tests/exec_kunit.c"
> >  #endif
> > diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
> > index 2c77e383e..17419cd3d 100644
> > --- a/include/linux/binfmts.h
> > +++ b/include/linux/binfmts.h
> > @@ -150,4 +150,6 @@ extern ssize_t read_code(struct file *, unsigned long, loff_t, size_t);
> >  int kernel_execve(const char *filename,
> >                 const char *const *argv, const char *const *envp);
> >
> > +char *resolve_elf_interpreter(struct linux_binprm *bprm, const char *elf_interpreter);
> > +
> >  #endif /* _LINUX_BINFMTS_H */
> > --
> > 2.51.2
> >
>
> So, I guess, I'd like more convincing, but I'm very happy to see a KUnit
> test included!
>
> -Kees
>
> --
> Kees Cook


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-22 21:08     ` John Ericson
@ 2026-06-25  8:50       ` Christian Brauner
  2026-06-25 19:34         ` John Ericson
  2026-06-26 12:39         ` Jann Horn
  0 siblings, 2 replies; 29+ messages in thread
From: Christian Brauner @ 2026-06-25  8:50 UTC (permalink / raw)
  To: John Ericson
  Cc: Farid Zakaria, Jan Kara, Kees Cook, Christian Brauner, Al Viro,
	shuah, linux-fsdevel, linux-mm, linux-kselftest, LKML

On 2026-06-22 17:08:55-04:00, John Ericson wrote:
> Hi, I am another Nix developer, and have participated in some LKML
> discussions in the (recent and distant) past, and thought I should weigh
> in here too.
> 
> On Mon, Jun 22, 2026, at 1:15 PM, Farid Zakaria wrote:
> 
> > On Mon, Jun 22, 2026 at 3:40 AM Jan Kara <jack@suse.cz> wrote:
> >
> > Having put forward the patch, I'm clearly biased toward thinking this
> > support should exist in the kernel.
> > If I had to think to strengthen my argument would be that the kernel
> > should not be imposing how the interpreter is found on userland.
> > Finding the interpreter relative to the binary would be useful for
> > package deployment scenarios similar to app-bundles beyond systems
> > like Nix -- which is the originating reason why $ORIGIN exists in the
> > dynamic linker.
> 
> Yes, the idea of making "relocatable software" is not a new one, and
> indeed it is why `$ORIGIN` is supported in the RPATH etc. in the first
> place.
> 
> Most of the programming model for writing relocatable software is fixed
> at this point. For example, /proc/self/exe made it much easier to look
> up arbitrary stuff relevant to the current executable. It is just some
> initial entry point stuff (the ELF interpreter, and shebangs) which is a
> glaring exception. Those should support `$ORIGIN` too. There is no good
> technical justification (that I can think of) for some but not all of
> these supporting `$ORIGIN` --- either it makes sense everywhere, or it
> makes sense nowhere.
> 
> (I suspect the only reason it didn't happen was pure inertia/Conway's
> law --- easier for whoever was excited about `$ORIGIN` to change the
> glibc loader than the kernel.)
> 
> > To me, the gap is that prior to systems like Nix, the idea of wanting
> > your dynamic linker to be part of your app bundle was not necessary
> > but Nix models the dependency chain down to the loader. Such
> > functionality would be even more correct for these other bundled
> > solutions as well, making them portable across userspace glibc
> > versions for instance.
> 
> Yes, exactly. Traditionally people thought "eh `/lib/ld-linux.so.*`
> doesn't change too much", and decided relocatable software that
> nonetheless hard-coded that absolute path to an unknown system-provided
> ELF interpreter was good enough. (Or if they weren't good enough, they
> went with static linking, but that imposes other costs.)
> 
> Now there do exist purely-user-space work-arounds, like
> https://github.com/Mic92/wrap-buddy, but they are quite complex, and
> involve various patching trickery that is likely to scare a lot of
> security analysis tools. A kernel-based solution that allows clean
> declarative expression of intent with `$ORIGIN` is much more elegant.
> 
> > > In particular the
> 
> I think it is good to see what Conda does as documented in
> <https://docs.conda.io/projects/conda-build/en/stable/resources/make-relocatable.html>
> and consider why relying on namespaces vs good old-fashioned relocatable
> isn't good enough for them either.
> 
> (I don't doubt that Conda would find this approach more robust than
> their sedding tricks, and prefer to use it where possible.)
> 
> The short answer is while all of us in the build system space love
> sandboxing during the build, we don't want that to lead to *requiring*
> run time sandboxing of the built artifacts. For example, we can
> certainly arrange sandboxing so `/lib/ld-linux.so.*` is the one that
> some executable expects now, but every time that executable is run, it
> *must* be run in a root filesystem where `/lib/ld-linux.so.*` is the
> loader it expects.
> 
> If you have multiple programs that (for whatever reasons) expect
> multiple different loaders, all spawning one another, it would
> potentially incur quite the development cost to ensure that they all do
> the proper unsharing to make everything work.
> 
> Relocatability recognizes that whether or not namespaces exist, in an
> "open world" scenario where we don't know how the software we are
> writing will be combined with other software for deployment downstream
> in different ways, it is easiest to adopt an idiom where different
> things can be placed at different absolute paths, at the user's
> discretion, and so conflicts are always avoidable.
> 
> > > Anyway I'm pretty sure Christian will have more educated answer than me but
> 
> Waiting makes sense, I am curious too what he will have to say.

The arguments I have heard from various people so far are:

(1) Userspace would be able to clone a random chroot to /woot and run a
    binary from it without having to set up a complicated sandbox
    effectively making dynamically linked binaries more like static
    binaries in a sense.

(2) Quote:
    "If you debootstrap/dnf a chroot to some location in your
    home dir and try to run a binary from it, that it tries to load the
    libraries from your /usr is a pretty unintuitive and not at all
    useful behavior."

(3) Quote:
    "[Various remote execution things run in locked down containers that
    disable userns, which makes the sandbox impossible and hence our
    builds wouldn't work there."

I'm discounting "Oh, userspace already allows this so why not the
kernel.". I think that's generally a bad argument. Kernel and userspace
aren't really alike in that regard.

The userspace ORIGIN concept is guarded behind AT_SECURE. The kernel has
to enforce the same rule. That means the loader now depends on the type
of binary. I think this is a rather serious issue.

First, it creates confusion in userspace what loader is used. Second, it
means anything that any build/chroot that uses AT_SECURE binaries now
has to use the sandboxing solution anyway or risk that some binaries use
the system loader and others the chroot loader.

Ignoring AT_SECURE, LSMs likely will need a say in whether that ORIGIN
thing gets honored or not introducing yet another vector where this can
be overriden or ignored.

Also, we change long-standing kernel behavior which will be very
surprising for any userspace that might implicitly rely on the fact that
the system loader is used. So even if we were to do something like this
it would very likely have to be configurable in some way.

This makes this all ripe for malicious loader injection attacks. And we
need to consider this possibility.

So I'm not enthusiastic about this. I want this to be consistent.


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-25  8:50       ` Christian Brauner
@ 2026-06-25 19:34         ` John Ericson
  2026-06-26 12:39         ` Jann Horn
  1 sibling, 0 replies; 29+ messages in thread
From: John Ericson @ 2026-06-25 19:34 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Farid Zakaria, Jan Kara, Kees Cook, Al Viro, shuah, linux-fsdevel,
	linux-mm, linux-kselftest, LKML

On Thu, Jun 25, 2026, at 4:50 AM, Christian Brauner wrote:
> Also, we change long-standing kernel behavior which will be very
> surprising for any userspace that might implicitly rely on the fact
> that the system loader is used. So even if we were to do something
> like this it would very likely have to be configurable in some way.

Yes that's definitely something that worried me.

> So I'm not enthusiastic about this. I want this to be consistent.

For what it's worth, I somewhat wish the `PT_INTERP` field didn't exist
at all. IMO the ELF spec is very weird for some parts of the same
headers being for the kernel, and the other parts being for userspace.
If there were separate data structures (think encapsulated packets) so
it was crystal clear who was responsible for what, we wouldn't be in
this situation.

While I earlier complained that our
<https://github.com/Mic92/wrap-buddy> work-around was likely to make
some security analyses/policies go haywire, arguably it implements this
behavior: from the kernel's perspective, everything is statically
linked, and the program, entirely in userspace, takes responsibility for
whatever loading it needs.

Put that way, this is less of an ugly work-around than the way I wish
things worked period in a world without `PT_INTERP`. Dynamic linking
would be entirely a userspace concern and the kernel would not know or
care.

----

What about shebangs? It's the same problem, with the same solutions and
pitfalls. We can also make wrapper executables around them that do the
right thing, but turning mere scripts into statically linked binaries is
a bit heavier. I assume the answer is the same, but just want to double
check before we close this off.

Cheers,

John

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-25  8:50       ` Christian Brauner
  2026-06-25 19:34         ` John Ericson
@ 2026-06-26 12:39         ` Jann Horn
  2026-06-26 13:26           ` David Laight
  2026-06-28 12:36           ` Christian Brauner
  1 sibling, 2 replies; 29+ messages in thread
From: Jann Horn @ 2026-06-26 12:39 UTC (permalink / raw)
  To: Christian Brauner
  Cc: John Ericson, Farid Zakaria, Jan Kara, Kees Cook, Al Viro, shuah,
	linux-fsdevel, linux-mm, linux-kselftest, LKML

On Thu, Jun 25, 2026 at 10:50 AM Christian Brauner <brauner@kernel.org> wrote:
> The arguments I have heard from various people so far are:
>
> (1) Userspace would be able to clone a random chroot to /woot and run a
>     binary from it without having to set up a complicated sandbox
>     effectively making dynamically linked binaries more like static
>     binaries in a sense.
>
> (2) Quote:
>     "If you debootstrap/dnf a chroot to some location in your
>     home dir and try to run a binary from it, that it tries to load the
>     libraries from your /usr is a pretty unintuitive and not at all
>     useful behavior."
>
> (3) Quote:
>     "[Various remote execution things run in locked down containers that
>     disable userns, which makes the sandbox impossible and hence our
>     builds wouldn't work there."

FWIW I think someone also mentioned to me that it would make things
easier for them if they could build a piece of software in one
environment and then bundle it up with all required libraries and such
and run it in a very different environment, without
container/sandboxing stuff and without static linking. But I guess
that's kinda niche.

> I'm discounting "Oh, userspace already allows this so why not the
> kernel.". I think that's generally a bad argument. Kernel and userspace
> aren't really alike in that regard.
>
> The userspace ORIGIN concept is guarded behind AT_SECURE. The kernel has

(To be pedantic: The userspace $ORIGIN concept is only partially gated
on AT_SECURE - glibc has an allowlist of acceptable library
directories, listed in "/lib64/ld-linux-x86-64.so.2 --list-diagnostics
| grep ^path.system_dirs". But clearly we wouldn't want to mirror that
in the kernel.)

> to enforce the same rule. That means the loader now depends on the type
> of binary. I think this is a rather serious issue.

And annoyingly, the bprm->secureexec flag can change in
security_bprm_creds_from_file(), which is currently reached from
begin_new_exec(), which is called after we've already opened the
interpreter, so accessing ->secureexec state during the interpreter
lookup would require some refactoring. So I think this is a doable
change, but would require more work.

Or we could take the easy way out and say "the kernel always rejects
this unless LSM_UNSAFE_NO_NEW_PRIVS is set", which would make it clear
that this can't lead to privilege escalation and would serve as an
incentive for people to stop doing stuff that relies on setuid
binaries or privileged apparmor/selinux transitions. :P

> First, it creates confusion in userspace what loader is used. Second, it
> means anything that any build/chroot that uses AT_SECURE binaries now
> has to use the sandboxing solution anyway or risk that some binaries use
> the system loader and others the chroot loader.

I think we would probably just fail the execve() attempt if we see
$ORIGIN in the interpreter in an AT_SECURE execution? Since the
interpreter field does not allow listing multiple alternatives.

> Ignoring AT_SECURE, LSMs likely will need a say in whether that ORIGIN
> thing gets honored or not introducing yet another vector where this can
> be overriden or ignored.
>
> Also, we change long-standing kernel behavior which will be very
> surprising for any userspace that might implicitly rely on the fact that
> the system loader is used. So even if we were to do something like this
> it would very likely have to be configurable in some way.

I think the proposed patch will only change behavior if the
interpreter path starts with "$ORIGIN"? That wouldn't work on existing
kernels unless you have a directory literally named "$ORIGIN" in the
cwd, because "$ORIGIN/..." would be interpreted as a normal relative
path.

> This makes this all ripe for malicious loader injection attacks. And we
> need to consider this possibility.
>
> So I'm not enthusiastic about this. I want this to be consistent.


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-26 12:39         ` Jann Horn
@ 2026-06-26 13:26           ` David Laight
  2026-06-26 13:34             ` Jann Horn
  2026-06-28 12:36           ` Christian Brauner
  1 sibling, 1 reply; 29+ messages in thread
From: David Laight @ 2026-06-26 13:26 UTC (permalink / raw)
  To: Jann Horn
  Cc: Christian Brauner, John Ericson, Farid Zakaria, Jan Kara,
	Kees Cook, Al Viro, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, LKML

On Fri, 26 Jun 2026 14:39:22 +0200
Jann Horn <jannh@google.com> wrote:

> On Thu, Jun 25, 2026 at 10:50 AM Christian Brauner <brauner@kernel.org> wrote:
> > The arguments I have heard from various people so far are:
> >
> > (1) Userspace would be able to clone a random chroot to /woot and run a
> >     binary from it without having to set up a complicated sandbox
> >     effectively making dynamically linked binaries more like static
> >     binaries in a sense.
> >
> > (2) Quote:
> >     "If you debootstrap/dnf a chroot to some location in your
> >     home dir and try to run a binary from it, that it tries to load the
> >     libraries from your /usr is a pretty unintuitive and not at all
> >     useful behavior."
> >
> > (3) Quote:
> >     "[Various remote execution things run in locked down containers that
> >     disable userns, which makes the sandbox impossible and hence our
> >     builds wouldn't work there."  
> 
> FWIW I think someone also mentioned to me that it would make things
> easier for them if they could build a piece of software in one
> environment and then bundle it up with all required libraries and such
> and run it in a very different environment, without
> container/sandboxing stuff and without static linking. But I guess
> that's kinda niche.

The problem with 'ship the shared libraries with the application' is
that you get all the problems of static linking.
If there is a bug in the library code you can't fix it without getting the
3rd party to rebuild their application package.
If the bug is in a system shared library updating the system libraries
fixes the bug.
Now this does require that the writers of shared libraries maintain
backwards compatibility and that the 'system' provides the required updates.

I remember a long time ago the company I worked for shipped a system where
the libc.so the linker found was actually an archive library one of whose
members was a shared library.
So some functions were dynamically loaded and others static.
There was a bug in one of the static functions (IIRC it corrupted the utmp
file), once located and fixed the 3rd party had to be persuaded to rebuild
and re-release their product.
(It has to be said that anyone with half a brain would have realised that
because libc was split for compatibility reasons, statically linking this
particular function was actually stupid.)

	David


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-26 13:26           ` David Laight
@ 2026-06-26 13:34             ` Jann Horn
  2026-06-26 13:40               ` Farid Zakaria
  2026-06-26 16:28               ` David Laight
  0 siblings, 2 replies; 29+ messages in thread
From: Jann Horn @ 2026-06-26 13:34 UTC (permalink / raw)
  To: David Laight
  Cc: Christian Brauner, John Ericson, Farid Zakaria, Jan Kara,
	Kees Cook, Al Viro, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, LKML

On Fri, Jun 26, 2026 at 3:26 PM David Laight
<david.laight.linux@gmail.com> wrote:
> On Fri, 26 Jun 2026 14:39:22 +0200
> Jann Horn <jannh@google.com> wrote:
>
> > On Thu, Jun 25, 2026 at 10:50 AM Christian Brauner <brauner@kernel.org> wrote:
> > > The arguments I have heard from various people so far are:
> > >
> > > (1) Userspace would be able to clone a random chroot to /woot and run a
> > >     binary from it without having to set up a complicated sandbox
> > >     effectively making dynamically linked binaries more like static
> > >     binaries in a sense.
> > >
> > > (2) Quote:
> > >     "If you debootstrap/dnf a chroot to some location in your
> > >     home dir and try to run a binary from it, that it tries to load the
> > >     libraries from your /usr is a pretty unintuitive and not at all
> > >     useful behavior."
> > >
> > > (3) Quote:
> > >     "[Various remote execution things run in locked down containers that
> > >     disable userns, which makes the sandbox impossible and hence our
> > >     builds wouldn't work there."
> >
> > FWIW I think someone also mentioned to me that it would make things
> > easier for them if they could build a piece of software in one
> > environment and then bundle it up with all required libraries and such
> > and run it in a very different environment, without
> > container/sandboxing stuff and without static linking. But I guess
> > that's kinda niche.
>
> The problem with 'ship the shared libraries with the application' is
> that you get all the problems of static linking.
> If there is a bug in the library code you can't fix it without getting the
> 3rd party to rebuild their application package.

Yes, it's appropriate for weird use cases like "I want to run this
historical version of the software and its dependencies", it's not
necessarily a good idea for normal application use.

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-26 13:34             ` Jann Horn
@ 2026-06-26 13:40               ` Farid Zakaria
  2026-06-26 16:28               ` David Laight
  1 sibling, 0 replies; 29+ messages in thread
From: Farid Zakaria @ 2026-06-26 13:40 UTC (permalink / raw)
  To: Jann Horn
  Cc: David Laight, Christian Brauner, John Ericson, Jan Kara,
	Kees Cook, Al Viro, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, LKML

[-- Attachment #1: Type: text/plain, Size: 2983 bytes --]

It's very tough not to get too idealogical but please forgive me a little
as a Nix user.

What you are describing is a feature and not a bug. In Nix and similar
store-based systems the goal is purity and reproduction. Security
vulnerabilities are solved by potentially rebuilding the world if they are
sufficiently deep in the dependency graph.

The flip side to this deployment strategy is that it's very easy to
understand and audit one's system. By not linking to global library
directories, you cut down on a complete vector of abuse or system wide
updates that maybe make one vulnerable.

Back to the discussion at hand :)
Glad to see some discussion. This is something I continue to think fits
well in the kernel and this strategy of deployment is becoming increasingly
popular with tools like Bazel and Buck as well.

I continue to wait and am ready to make any necessary changes.


On Fri, Jun 26, 2026, 7:34 AM Jann Horn <jannh@google.com> wrote:

> On Fri, Jun 26, 2026 at 3:26 PM David Laight
> <david.laight.linux@gmail.com> wrote:
> > On Fri, 26 Jun 2026 14:39:22 +0200
> > Jann Horn <jannh@google.com> wrote:
> >
> > > On Thu, Jun 25, 2026 at 10:50 AM Christian Brauner <brauner@kernel.org>
> wrote:
> > > > The arguments I have heard from various people so far are:
> > > >
> > > > (1) Userspace would be able to clone a random chroot to /woot and
> run a
> > > >     binary from it without having to set up a complicated sandbox
> > > >     effectively making dynamically linked binaries more like static
> > > >     binaries in a sense.
> > > >
> > > > (2) Quote:
> > > >     "If you debootstrap/dnf a chroot to some location in your
> > > >     home dir and try to run a binary from it, that it tries to load
> the
> > > >     libraries from your /usr is a pretty unintuitive and not at all
> > > >     useful behavior."
> > > >
> > > > (3) Quote:
> > > >     "[Various remote execution things run in locked down containers
> that
> > > >     disable userns, which makes the sandbox impossible and hence our
> > > >     builds wouldn't work there."
> > >
> > > FWIW I think someone also mentioned to me that it would make things
> > > easier for them if they could build a piece of software in one
> > > environment and then bundle it up with all required libraries and such
> > > and run it in a very different environment, without
> > > container/sandboxing stuff and without static linking. But I guess
> > > that's kinda niche.
> >
> > The problem with 'ship the shared libraries with the application' is
> > that you get all the problems of static linking.
> > If there is a bug in the library code you can't fix it without getting
> the
> > 3rd party to rebuild their application package.
>
> Yes, it's appropriate for weird use cases like "I want to run this
> historical version of the software and its dependencies", it's not
> necessarily a good idea for normal application use.
>

[-- Attachment #2: Type: text/html, Size: 4147 bytes --]

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-26 13:34             ` Jann Horn
  2026-06-26 13:40               ` Farid Zakaria
@ 2026-06-26 16:28               ` David Laight
  1 sibling, 0 replies; 29+ messages in thread
From: David Laight @ 2026-06-26 16:28 UTC (permalink / raw)
  To: Jann Horn
  Cc: Christian Brauner, John Ericson, Farid Zakaria, Jan Kara,
	Kees Cook, Al Viro, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, LKML

On Fri, 26 Jun 2026 15:34:12 +0200
Jann Horn <jannh@google.com> wrote:

> On Fri, Jun 26, 2026 at 3:26 PM David Laight
> <david.laight.linux@gmail.com> wrote:
> > On Fri, 26 Jun 2026 14:39:22 +0200
> > Jann Horn <jannh@google.com> wrote:
> >  
> > > On Thu, Jun 25, 2026 at 10:50 AM Christian Brauner <brauner@kernel.org> wrote:  
> > > > The arguments I have heard from various people so far are:
> > > >
> > > > (1) Userspace would be able to clone a random chroot to /woot and run a
> > > >     binary from it without having to set up a complicated sandbox
> > > >     effectively making dynamically linked binaries more like static
> > > >     binaries in a sense.
> > > >
> > > > (2) Quote:
> > > >     "If you debootstrap/dnf a chroot to some location in your
> > > >     home dir and try to run a binary from it, that it tries to load the
> > > >     libraries from your /usr is a pretty unintuitive and not at all
> > > >     useful behavior."
> > > >
> > > > (3) Quote:
> > > >     "[Various remote execution things run in locked down containers that
> > > >     disable userns, which makes the sandbox impossible and hence our
> > > >     builds wouldn't work there."  
> > >
> > > FWIW I think someone also mentioned to me that it would make things
> > > easier for them if they could build a piece of software in one
> > > environment and then bundle it up with all required libraries and such
> > > and run it in a very different environment, without
> > > container/sandboxing stuff and without static linking. But I guess
> > > that's kinda niche.  
> >
> > The problem with 'ship the shared libraries with the application' is
> > that you get all the problems of static linking.
> > If there is a bug in the library code you can't fix it without getting the
> > 3rd party to rebuild their application package.  
> 
> Yes, it's appropriate for weird use cases like "I want to run this
> historical version of the software and its dependencies", it's not
> necessarily a good idea for normal application use.

That's what LD_LIBRARY_PATH is for ...

And if you want to use a different elf interpreter just run it and pass the
program name and arguments to it. eg:
	/lib64/ld-linux-x64-64.so.2 /bin/echo fubar
Last time I did that I was trying to run non-linux ppc elf program.
I got part way there, but needed to build a lot more of libc.

	David

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-26 12:39         ` Jann Horn
  2026-06-26 13:26           ` David Laight
@ 2026-06-28 12:36           ` Christian Brauner
  2026-06-28 13:20             ` Christian Brauner
  1 sibling, 1 reply; 29+ messages in thread
From: Christian Brauner @ 2026-06-28 12:36 UTC (permalink / raw)
  To: Jann Horn
  Cc: Christian Brauner, John Ericson, Farid Zakaria, Jan Kara,
	Kees Cook, Al Viro, shuah, linux-fsdevel, linux-mm,
	linux-kselftest, LKML

> > Ignoring AT_SECURE, LSMs likely will need a say in whether that ORIGIN
> > thing gets honored or not introducing yet another vector where this can
> > be overriden or ignored.
> >
> > Also, we change long-standing kernel behavior which will be very
> > surprising for any userspace that might implicitly rely on the fact that
> > the system loader is used. So even if we were to do something like this
> > it would very likely have to be configurable in some way.
> 
> I think the proposed patch will only change behavior if the
> interpreter path starts with "$ORIGIN"? That wouldn't work on existing
> kernels unless you have a directory literally named "$ORIGIN" in the
> cwd, because "$ORIGIN/..." would be interpreted as a normal relative
> path.

I was thinking:

You download a bunch of $ORIGIN binaries into /woot on a kernel that
doesn't support $ORIGIN. You forget about /woot and update your kernel.
You have no idea that kernel behavior has changed to allow $ORIGIN to
work for the loader. You remember /woot exists and are starting to
execute binaries under the assumption that $ORIGIN is purely a userspace
concept applicable to shared libraries.

/woot came with a malicious loader and it suddenly gets used - if that
happens in a sandbox noboday cares. Now that $ORIGIN works and no
sandbox might be needed I think enabling $ORIGIN might still end up with
funny surprises.


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-28 12:36           ` Christian Brauner
@ 2026-06-28 13:20             ` Christian Brauner
  2026-06-28 18:44               ` David Laight
                                 ` (2 more replies)
  0 siblings, 3 replies; 29+ messages in thread
From: Christian Brauner @ 2026-06-28 13:20 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Jann Horn, John Ericson, Farid Zakaria, Jan Kara, Kees Cook,
	Al Viro, shuah, linux-fsdevel, linux-mm, linux-kselftest, LKML

On 2026-06-28 14:36:52+02:00, Christian Brauner wrote:
> > I think the proposed patch will only change behavior if the
> > interpreter path starts with "$ORIGIN"? That wouldn't work on existing
> > kernels unless you have a directory literally named "$ORIGIN" in the
> > cwd, because "$ORIGIN/..." would be interpreted as a normal relative
> > path.
> 
> I was thinking:

A few other things came to my mind:

(1) memfds: /memfd:woot-woot

    A memfd doesn't have a path so any $ORIGIN type behavior - script or not -
    will be at least useless if not very confusing outside of a sandbox.

    Btw, used by runC to guard against binary overwrite attacks.

(1.1) deleted memfds: /memfd:woot-woot (deleted)

      Same as (1).

(2) deleted executables:

    /woot/woot-woot (deleted)

(3) unrecoverable executable paths

    No path can be resolved at all. What to do?

All of that needs consistent, easy to reason about treatment. Reading through
the glibc implementation of $ORIGIN for shared libraries in rpath - even with
an eye on cutting through most of the complexity - doesn't give me very warm
feelings. It feels very hackish and full of edge cases...

Imho, tying the lookup of the interpreter to the binary itself and making the
kernel responsible for figuring out the relationship by resolving bprm->file
and splicing it with PT_INTERP is terrible.

So, I kinda like the concept of having relocatable dynamic executables but then
let's cut through all the userspace red tape and figure out something that's
super simple and outsources the problem to userspace.

I think the wrap-buddy approach here: https://github.com/Mic92/wrap-buddy is
going in the right direction in that it gets the kernel completely out of the
way. I like that a lot more than moving more nasty bits into the kernel itself.

In a way we have been doing something in systemd that goes in a similar
direction. As of systemd 261 systemd _only_ links against libc and nothing
else.

Any other shared library is dlopen()ened as needed (discoverable via
elf-notes). Lennart wrote about this just a few days ago:
https://mastodon.social/@pid_eins/116781776665322560

This effectively minimizes the work the loader has to do at startup. Imho, your
effort with wrap-buddy is related. To me moving the loader invocation out of
the kernel and into userspace makes a lot of sense to me.


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-28 13:20             ` Christian Brauner
@ 2026-06-28 18:44               ` David Laight
  2026-06-30 16:58               ` Farid Zakaria
  2026-06-30 17:59               ` David Laight
  2 siblings, 0 replies; 29+ messages in thread
From: David Laight @ 2026-06-28 18:44 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Jann Horn, John Ericson, Farid Zakaria, Jan Kara, Kees Cook,
	Al Viro, shuah, linux-fsdevel, linux-mm, linux-kselftest, LKML

On Sun, 28 Jun 2026 15:20:11 +0200
Christian Brauner <brauner@kernel.org> wrote:

...
> All of that needs consistent, easy to reason about treatment. Reading through
> the glibc implementation of $ORIGIN for shared libraries in rpath - even with
> an eye on cutting through most of the complexity - doesn't give me very warm
> feelings. It feels very hackish and full of edge cases...

I'm sure that NetBSD refused to implement $ORIGIN because it was full
of loopholes.
You'd need the kernel to hold a reference to the directory inode and
have a 'magic' argument to openat() to be relative to that directory
rather than cwd.

	David

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-28 13:20             ` Christian Brauner
  2026-06-28 18:44               ` David Laight
@ 2026-06-30 16:58               ` Farid Zakaria
  2026-07-02  6:47                 ` Christian Brauner
  2026-06-30 17:59               ` David Laight
  2 siblings, 1 reply; 29+ messages in thread
From: Farid Zakaria @ 2026-06-30 16:58 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Jann Horn, John Ericson, Jan Kara, Kees Cook, Al Viro, shuah,
	linux-fsdevel, linux-mm, linux-kselftest, LKML

On Sun, Jun 28, 2026 at 6:20 AM Christian Brauner <brauner@kernel.org> wrote:
>
> On 2026-06-28 14:36:52+02:00, Christian Brauner wrote:
> > > I think the proposed patch will only change behavior if the
> > > interpreter path starts with "$ORIGIN"? That wouldn't work on existing
> > > kernels unless you have a directory literally named "$ORIGIN" in the
> > > cwd, because "$ORIGIN/..." would be interpreted as a normal relative
> > > path.
> >
> > I was thinking:
>
> A few other things came to my mind:
>
> (1) memfds: /memfd:woot-woot
>
>     A memfd doesn't have a path so any $ORIGIN type behavior - script or not -
>     will be at least useless if not very confusing outside of a sandbox.
>
>     Btw, used by runC to guard against binary overwrite attacks.
>
> (1.1) deleted memfds: /memfd:woot-woot (deleted)
>
>       Same as (1).
>
> (2) deleted executables:
>
>     /woot/woot-woot (deleted)
>
> (3) unrecoverable executable paths
>
>     No path can be resolved at all. What to do?
>
> All of that needs consistent, easy to reason about treatment. Reading through
> the glibc implementation of $ORIGIN for shared libraries in rpath - even with
> an eye on cutting through most of the complexity - doesn't give me very warm
> feelings. It feels very hackish and full of edge cases...
>
> Imho, tying the lookup of the interpreter to the binary itself and making the
> kernel responsible for figuring out the relationship by resolving bprm->file
> and splicing it with PT_INTERP is terrible.
>
> So, I kinda like the concept of having relocatable dynamic executables but then
> let's cut through all the userspace red tape and figure out something that's
> super simple and outsources the problem to userspace.
>
> I think the wrap-buddy approach here: https://github.com/Mic92/wrap-buddy is
> going in the right direction in that it gets the kernel completely out of the
> way. I like that a lot more than moving more nasty bits into the kernel itself.
>
> In a way we have been doing something in systemd that goes in a similar
> direction. As of systemd 261 systemd _only_ links against libc and nothing
> else.
>
> Any other shared library is dlopen()ened as needed (discoverable via
> elf-notes). Lennart wrote about this just a few days ago:
> https://mastodon.social/@pid_eins/116781776665322560
>
> This effectively minimizes the work the loader has to do at startup. Imho, your
> effort with wrap-buddy is related. To me moving the loader invocation out of
> the kernel and into userspace makes a lot of sense to me.
>

Wow the systemd approach sounds pretty surprising. As a Nix/NixOS user
having software enable/disable features depending on changes to the
system ad-hoc seems like a footgun. Despite the allure of shared
objects, the are less re-used in practice and rebuilding them when a
CVE appears is just as easy.
A good read to me:
https://web.archive.org/web/20260320072121/https://drewdevault.com/dynlib.html
https://lore.kernel.org/lkml/CAHk-=whs8QZf3YnifdLv57+FhBi5_WeNTG1B-suOES=RcUSmQg@mail.gmail.com/
(yes true, NixOS also uses "shared libraries" but they are effectively
static in how they are setup with fixed paths).

Anyway, I digress.
What other options are there to go after for a relocatable binary? The
steps wrap-buddy has to take... are a bit horrendous in practicality
(although amazing from an engineering-sense).

binfmt itself is configurable and modules can register additional exec
types. What about something similar for ELF & INTERP ?
Maybe we can meet-half way and support it via a plugin architecture
that NixOS can leverage.


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-28 13:20             ` Christian Brauner
  2026-06-28 18:44               ` David Laight
  2026-06-30 16:58               ` Farid Zakaria
@ 2026-06-30 17:59               ` David Laight
  2026-07-02 21:42                 ` [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry Farid Zakaria
  2 siblings, 1 reply; 29+ messages in thread
From: David Laight @ 2026-06-30 17:59 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Jann Horn, John Ericson, Farid Zakaria, Jan Kara, Kees Cook,
	Al Viro, shuah, linux-fsdevel, linux-mm, linux-kselftest, LKML

On Sun, 28 Jun 2026 15:20:11 +0200
Christian Brauner <brauner@kernel.org> wrote:

...
> In a way we have been doing something in systemd that goes in a similar
> direction. As of systemd 261 systemd _only_ links against libc and nothing
> else.
> 
> Any other shared library is dlopen()ened as needed (discoverable via
> elf-notes). Lennart wrote about this just a few days ago:
> https://mastodon.social/@pid_eins/116781776665322560
> 
> This effectively minimizes the work the loader has to do at startup. Imho, your
> effort with wrap-buddy is related. To me moving the loader invocation out of
> the kernel and into userspace makes a lot of sense to me.

I've done that for libraries that a program doesn't normally need.
But it doesn't make sense for a long-lived daemon.
The work almost certainly needs doing at some point.

The other problem is that it is hard to handle symbol versioning.
So the function you get may not match the one the header file
defined and you suffer the consequences.

	David


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths
  2026-06-30 16:58               ` Farid Zakaria
@ 2026-07-02  6:47                 ` Christian Brauner
  0 siblings, 0 replies; 29+ messages in thread
From: Christian Brauner @ 2026-07-02  6:47 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Christian Brauner, Jann Horn, John Ericson, Jan Kara, Kees Cook,
	Al Viro, shuah, linux-fsdevel, linux-mm, linux-kselftest, LKML

> Anyway, I digress.
> What other options are there to go after for a relocatable binary? The
> steps wrap-buddy has to take... are a bit horrendous in practicality
> (although amazing from an engineering-sense).
> 
> binfmt itself is configurable and modules can register additional exec
> types. What about something similar for ELF & INTERP ?

Not sure if my message made it yesterday. It somehow seems it didn't so
I'm repeating it: let's look at the code for what you had in mind.


^ permalink raw reply	[flat|nested] 29+ messages in thread

* [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry
  2026-06-30 17:59               ` David Laight
@ 2026-07-02 21:42                 ` Farid Zakaria
  2026-07-03  9:22                   ` Christian Brauner
  0 siblings, 1 reply; 29+ messages in thread
From: Farid Zakaria @ 2026-07-02 21:42 UTC (permalink / raw)
  To: david.laight.linux
  Cc: brauner, farid.m.zakaria, jack, jannh, kees, linux-fsdevel,
	linux-kernel, linux-kselftest, linux-mm, mail, shuah, viro

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 <farid.m.zakaria@gmail.com>
---

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 <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 000000000..d28b92c30
--- /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 b92fe7db1..45813bbce 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);
+
+		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 <linux/binfmts.h>
+#include <linux/elf.h>
+#include <linux/list.h>
+
+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


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* Re: [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry
  2026-07-02 21:42                 ` [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry Farid Zakaria
@ 2026-07-03  9:22                   ` Christian Brauner
  2026-07-03 18:32                     ` Farid Zakaria
  0 siblings, 1 reply; 29+ messages in thread
From: Christian Brauner @ 2026-07-03  9:22 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: david.laight.linux, brauner, jack, jannh, kees, linux-fsdevel,
	linux-kernel, linux-kselftest, linux-mm, mail, shuah, viro

> 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 <farid.m.zakaria@gmail.com>
>
> 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.

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.

-- 
Christian Brauner <brauner@kernel.org>

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry
  2026-07-03  9:22                   ` Christian Brauner
@ 2026-07-03 18:32                     ` Farid Zakaria
  2026-07-04  8:15                       ` Christian Brauner
  0 siblings, 1 reply; 29+ messages in thread
From: Farid Zakaria @ 2026-07-03 18:32 UTC (permalink / raw)
  To: Christian Brauner
  Cc: david.laight.linux, jack, jannh, kees, linux-fsdevel,
	linux-kernel, linux-kselftest, linux-mm, mail, shuah, viro

On Fri, Jul 3, 2026 at 2:22 AM Christian Brauner <brauner@kernel.org> 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 <farid.m.zakaria@gmail.com>
> >
> > 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 <brauner@kernel.org>


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry
  2026-07-03 18:32                     ` Farid Zakaria
@ 2026-07-04  8:15                       ` Christian Brauner
  2026-07-04 21:14                         ` [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection Farid Zakaria
  0 siblings, 1 reply; 29+ messages in thread
From: Christian Brauner @ 2026-07-04  8:15 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: Christian Brauner, david.laight.linux, jack, jannh, kees,
	linux-fsdevel, linux-kernel, linux-kselftest, linux-mm, mail,
	shuah, viro

On 2026-07-03 11:32 -0700, Farid Zakaria wrote:
> On Fri, Jul 3, 2026 at 2:22 AM Christian Brauner <brauner@kernel.org> 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 <farid.m.zakaria@gmail.com>
> > >
> > > 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.



^ permalink raw reply	[flat|nested] 29+ messages in thread

* [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection
  2026-07-04  8:15                       ` Christian Brauner
@ 2026-07-04 21:14                         ` Farid Zakaria
  2026-07-06 16:01                           ` Christian Brauner
  0 siblings, 1 reply; 29+ messages in thread
From: Farid Zakaria @ 2026-07-04 21:14 UTC (permalink / raw)
  To: brauner
  Cc: david.laight.linux, farid.m.zakaria, jack, jannh, kees,
	linux-fsdevel, linux-kernel, linux-kselftest, linux-mm, mail,
	shuah, viro, Gemini

Historically binfmt_misc has only matched binaries by magic bytes or file
extension, and always redirects a match to a fixed interpreter recorded in
the registration. This is insufficient when the match requires parsing the
binary header (e.g. inspecting ELF program headers) or when the interpreter
must be computed per-binary rather than hard-coded.

Introduce a new 'B' (BPF) handler type. A pinned SOCKET_FILTER program is
registered in place of the magic/mask, and no interpreter is recorded:

  echo ':name:B:<bpf_pinned_path>::::<flags>' \
      > /proc/sys/fs/binfmt_misc/register

When a binary is executed, binfmt_misc runs the program with the
BINPRM_BUF_SIZE file-header buffer as context. Returning 1 selects the
handler; returning 0 falls through to the remaining handlers.

Unlike magic/extension handlers, a 'B' handler carries no interpreter of its
own: the program chooses it via a new helper, bpf_binprm_set_interp(). This
lets the program compute the interpreter path however it sees fit (for example
relative to the binary). A 'B' handler is therefore a strict superset of the
existing magic handlers -- any of them can be expressed as a program that
matches on the header and sets a fixed interpreter.

bpf_binprm_set_interp() is exposed to SOCKET_FILTER programs and stashes the
chosen path on a per-CPU area that binfmt_misc reads back immediately after
the run under migrate_disable(); only a match that set an interpreter
allocates.

Assisted-by: Gemini <assistant@google.com>
Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
---

Hey Christian,

Thank you for the suggestion and discussion on the mailing list. I took a stab
at your idea of driving binfmt_misc interpreter selection from a BPF program
(hopefully this is what you had in mind). The other approach was also to use a
static interpreter during registration but this current approach feels right.

This prototype demonstrates binfmt_misc interpreter selection from a BPF program
so the interpreter can be *computed* (e.g. $ORIGIN-relative) rather than recorded
statically. It replaces my earlier "pluggable ELF interpreter loader registry" RFC.

As this is a first draft, a few things you probably will have notes on.
I tried my best but I am a novice here so I welcome your feedback.

 * The prototype uses socket filter type. This seemed the easiest to wire up to
   start. My guess is it should be a dedicated type (eg. BPF_PROG_TYPE_BINFMT).
 * bpf_binprm_set_interp() is a "classic helper", but that list
   is frozen in favour of kfuncs. A kfunc needs BTF and can't be called from
   the raw-bytecode selftest I wrote.
 * The program only gets bprm->buf today.
   That is enough to match on the ELF header but not to compute an
   $ORIGIN-relative path from the binary's location, which needs bprm->filename
 * I have some selftests that exercises match + program-chosen interpreter. I don't
   include them here yet since it seems unnecessary when discussing the idea.

To demo the functionality, I thought it would be neat to see how this is a superset
and can replace all the qemu binfmt usage [1].

  $ ./hello-aarch64;
  -bash: /etc/binfmt-demo/hello-aarch64: cannot execute binary file: Exec format error

  $ bpftool prog load filter.bpf.o /sys/fs/bpf/qemu type socket
  $ echo ':qemu-bpf:B:/sys/fs/bpf/qemu::::P' > /proc/sys/fs/binfmt_misc/register

  $ ./hello-aarch64
  AARCH64_RAN_VIA_QEMU

All the per-CPU guards were recommended by AI.

[1] https://gist.github.com/fzakaria/bef27d2e21b0e36ffccda1cbf417b636

 fs/binfmt_misc.c         | 181 ++++++++++++++++++++++++++++++++++++---
 include/linux/bpf.h      |   1 +
 include/uapi/linux/bpf.h |   1 +
 net/core/filter.c        |   8 ++
 4 files changed, 180 insertions(+), 11 deletions(-)

diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c
index 84349fcb9..cf6698d59 100644
--- a/fs/binfmt_misc.c
+++ b/fs/binfmt_misc.c
@@ -29,6 +29,11 @@
 #include <linux/fs.h>
 #include <linux/uaccess.h>
 
+#ifdef CONFIG_BPF_SYSCALL
+#include <linux/bpf.h>
+#include <linux/filter.h>
+#endif
+
 #include "internal.h"
 
 #ifdef DEBUG
@@ -41,12 +46,14 @@ enum {
 	VERBOSE_STATUS = 1 /* make it zero to save 400 bytes kernel memory */
 };
 
-enum {Enabled, Magic};
+enum {Enabled, Magic, Bpf};
 #define MISC_FMT_PRESERVE_ARGV0 (1UL << 31)
 #define MISC_FMT_OPEN_BINARY (1UL << 30)
 #define MISC_FMT_CREDENTIALS (1UL << 29)
 #define MISC_FMT_OPEN_FILE (1UL << 28)
 
+struct bpf_prog;
+
 typedef struct {
 	struct list_head list;
 	unsigned long flags;		/* type, status, etc. */
@@ -59,6 +66,9 @@ typedef struct {
 	struct dentry *dentry;
 	struct file *interp_file;
 	refcount_t users;		/* sync removal with load_misc_binary() */
+#ifdef CONFIG_BPF_SYSCALL
+	struct bpf_prog *bpf_prog;
+#endif
 } Node;
 
 static struct file_system_type bm_fs_type;
@@ -78,10 +88,51 @@ static struct file_system_type bm_fs_type;
  */
 #define MAX_REGISTER_LENGTH 1920
 
+#ifdef CONFIG_BPF_SYSCALL
+struct binfmt_bpf_interp {
+	char path[PATH_MAX];
+	int len;	/* < 0 if the current program set no interpreter */
+};
+static DEFINE_PER_CPU(struct binfmt_bpf_interp, binfmt_bpf_interp);
+
+/*
+ * bpf_binprm_set_interp - let a binfmt_misc 'B' program pick the interpreter.
+ * @path: interpreter path, in BPF-accessible memory
+ * @len:  number of bytes in @path
+ *
+ * The program computes the interpreter path however it sees fit (e.g. relative
+ * to the binary). The path is stashed on a per-CPU area that binfmt_misc reads
+ * back immediately after running the program under migrate_disable(), so it
+ * cannot race with another CPU.
+ */
+BPF_CALL_2(bpf_binprm_set_interp, const char *, path, u32, len)
+{
+	struct binfmt_bpf_interp *sc = this_cpu_ptr(&binfmt_bpf_interp);
+
+	if (len == 0 || len >= PATH_MAX)
+		return -EINVAL;
+	/* @path is @len bytes of BPF memory and is not NUL-terminated. */
+	memcpy(sc->path, path, len);
+	sc->path[len] = '\0';
+	sc->len = len;
+	return 0;
+}
+
+const struct bpf_func_proto bpf_binprm_set_interp_proto = {
+	.func		= bpf_binprm_set_interp,
+	.gpl_only	= false,
+	.ret_type	= RET_INTEGER,
+	.arg1_type	= ARG_PTR_TO_MEM | MEM_RDONLY,
+	.arg2_type	= ARG_CONST_SIZE,
+};
+#endif /* CONFIG_BPF_SYSCALL */
+
 /**
  * search_binfmt_handler - search for a binary handler for @bprm
  * @misc: handle to binfmt_misc instance
  * @bprm: binary for which we are looking for a handler
+ * @bpf_interp: receives a kmalloc'd interpreter path if a 'B' program chooses
+ *              one via bpf_binprm_set_interp(); the caller must kfree() it
  *
  * Search for a binary type handler for @bprm in the list of registered binary
  * type handlers.
@@ -89,7 +140,8 @@ static struct file_system_type bm_fs_type;
  * Return: binary type list entry on success, NULL on failure
  */
 static Node *search_binfmt_handler(struct binfmt_misc *misc,
-				   struct linux_binprm *bprm)
+				   struct linux_binprm *bprm,
+				   char **bpf_interp)
 {
 	char *p = strrchr(bprm->interp, '.');
 	Node *e;
@@ -103,6 +155,37 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc,
 		if (!test_bit(Enabled, &e->flags))
 			continue;
 
+		/* Do matching based on BPF if applicable. */
+		if (test_bit(Bpf, &e->flags)) {
+#ifdef CONFIG_BPF_SYSCALL
+			if (e->bpf_prog) {
+				struct binfmt_bpf_interp *sc;
+				u32 ret;
+
+				migrate_disable();
+				sc = this_cpu_ptr(&binfmt_bpf_interp);
+				sc->len = -1;
+
+				rcu_read_lock();
+				ret = bpf_prog_run(e->bpf_prog, bprm->buf);
+				rcu_read_unlock();
+
+				if (ret == 1 && sc->len > 0)
+					*bpf_interp = kmemdup_nul(sc->path,
+								  sc->len,
+								  GFP_ATOMIC);
+				migrate_enable();
+
+				pr_debug("binfmt_misc: ran BPF program for %s, ret = %u\n",
+					 bprm->filename, ret);
+
+				if (ret == 1)
+					return e;
+			}
+#endif
+			continue;
+		}
+
 		/* Do matching based on extension if applicable. */
 		if (!test_bit(Magic, &e->flags)) {
 			if (p && !strcmp(e->magic, p + 1))
@@ -139,12 +222,13 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc,
  * Return: binary type list entry on success, NULL on failure
  */
 static Node *get_binfmt_handler(struct binfmt_misc *misc,
-				struct linux_binprm *bprm)
+				struct linux_binprm *bprm,
+				char **bpf_interp)
 {
 	Node *e;
 
 	read_lock(&misc->entries_lock);
-	e = search_binfmt_handler(misc, bprm);
+	e = search_binfmt_handler(misc, bprm, bpf_interp);
 	if (e)
 		refcount_inc(&e->users);
 	read_unlock(&misc->entries_lock);
@@ -164,6 +248,10 @@ static void put_binfmt_handler(Node *e)
 	if (refcount_dec_and_test(&e->users)) {
 		if (e->flags & MISC_FMT_OPEN_FILE)
 			filp_close(e->interp_file, NULL);
+#ifdef CONFIG_BPF_SYSCALL
+		if (test_bit(Bpf, &e->flags) && e->bpf_prog)
+			bpf_prog_put(e->bpf_prog);
+#endif
 		kfree(e);
 	}
 }
@@ -206,15 +294,27 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	struct file *interp_file = NULL;
 	int retval = -ENOEXEC;
 	struct binfmt_misc *misc;
+	char *bpf_interp __free(kfree) = NULL;
+	const char *interpreter;
 
 	misc = load_binfmt_misc();
 	if (!misc->enabled)
 		return retval;
 
-	fmt = get_binfmt_handler(misc, bprm);
+	fmt = get_binfmt_handler(misc, bprm, &bpf_interp);
 	if (!fmt)
 		return retval;
 
+	/*
+	 * A 'B' (BPF) handler carries no interpreter of its own; the program
+	 * chooses it via bpf_binprm_set_interp(). Other handlers use the
+	 * interpreter recorded at registration.
+	 */
+	interpreter = bpf_interp ? bpf_interp : fmt->interpreter;
+	retval = -ENOEXEC;
+	if (!interpreter[0])
+		goto ret;
+
 	/* Need to be able to load the file after exec */
 	retval = -ENOENT;
 	if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
@@ -238,22 +338,27 @@ static int load_misc_binary(struct linux_binprm *bprm)
 	bprm->argc++;
 
 	/* add the interp as argv[0] */
-	retval = copy_string_kernel(fmt->interpreter, bprm);
+	retval = copy_string_kernel(interpreter, bprm);
 	if (retval < 0)
 		goto ret;
 	bprm->argc++;
 
 	/* Update interp in case binfmt_script needs it. */
-	retval = bprm_change_interp(fmt->interpreter, bprm);
+	retval = bprm_change_interp(interpreter, bprm);
 	if (retval < 0)
 		goto ret;
 
-	if (fmt->flags & MISC_FMT_OPEN_FILE) {
+	/*
+	 * The pre-opened interp_file (MISC_FMT_OPEN_FILE / 'F' flag) only
+	 * applies to the statically registered interpreter; a program-supplied
+	 * path is opened here.
+	 */
+	if ((fmt->flags & MISC_FMT_OPEN_FILE) && !bpf_interp) {
 		interp_file = file_clone_open(fmt->interp_file);
 		if (!IS_ERR(interp_file))
 			deny_write_access(interp_file);
 	} else {
-		interp_file = open_exec(fmt->interpreter);
+		interp_file = open_exec(interpreter);
 	}
 	retval = PTR_ERR(interp_file);
 	if (IS_ERR(interp_file))
@@ -404,6 +509,10 @@ static Node *create_entry(const char __user *buffer, size_t count)
 		pr_debug("register: type: M (magic)\n");
 		e->flags = (1 << Enabled) | (1 << Magic);
 		break;
+	case 'B':
+		pr_debug("register: type: B (bpf)\n");
+		e->flags = (1 << Enabled) | (1 << Bpf);
+		break;
 	default:
 		goto einval;
 	}
@@ -492,6 +601,45 @@ static Node *create_entry(const char __user *buffer, size_t count)
 				}
 			}
 		}
+	} else if (test_bit(Bpf, &e->flags)) {
+		/* Handle the 'B' (BPF) format. */
+		char *s;
+
+		/* The offset field actually holds the pinned BPF program path */
+		s = strchr(p, del);
+		if (!s)
+			goto einval;
+		*s++ = '\0';
+		e->magic = p; /* Keep path in e->magic */
+		pr_debug("register: bpf program path: %s\n", e->magic);
+
+#ifdef CONFIG_BPF_SYSCALL
+		e->bpf_prog = bpf_prog_get_type_path(e->magic, BPF_PROG_TYPE_SOCKET_FILTER);
+		if (IS_ERR(e->bpf_prog)) {
+			err = PTR_ERR(e->bpf_prog);
+			e->bpf_prog = NULL;
+			kfree(e);
+			return ERR_PTR(err);
+		}
+#else
+		goto einval;
+#endif
+
+		p = s;
+
+		/* The magic field is unused, must be empty */
+		s = strchr(p, del);
+		if (!s || p != s)
+			goto einval;
+		*s++ = '\0';
+		p = s;
+
+		/* The mask field is unused, must be empty */
+		s = strchr(p, del);
+		if (!s || p != s)
+			goto einval;
+		*s++ = '\0';
+		p = s;
 	} else {
 		/* Handle the 'E' (extension) format. */
 
@@ -524,8 +672,17 @@ static Node *create_entry(const char __user *buffer, size_t count)
 	if (!p)
 		goto einval;
 	*p++ = '\0';
-	if (!e->interpreter[0])
+	if (test_bit(Bpf, &e->flags)) {
+		/*
+		 * A 'B' (BPF) handler carries no interpreter of its own; the
+		 * program picks it via bpf_binprm_set_interp(). Reject a
+		 * statically registered one.
+		 */
+		if (e->interpreter[0])
+			goto einval;
+	} else if (!e->interpreter[0]) {
 		goto einval;
+	}
 	pr_debug("register: interpreter: {%s}\n", e->interpreter);
 
 	/* Parse the 'flags' field. */
@@ -602,7 +759,9 @@ static void entry_status(Node *e, char *page)
 		*dp++ = 'F';
 	*dp++ = '\n';
 
-	if (!test_bit(Magic, &e->flags)) {
+	if (test_bit(Bpf, &e->flags)) {
+		sprintf(dp, "bpf %s\n", e->magic);
+	} else if (!test_bit(Magic, &e->flags)) {
 		sprintf(dp, "extension .%s\n", e->magic);
 	} else {
 		dp += sprintf(dp, "offset %i\nmagic ", e->offset);
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 7719f6528..3aef44b69 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3874,6 +3874,7 @@ extern const struct bpf_func_proto bpf_ktime_get_ns_proto;
 extern const struct bpf_func_proto bpf_ktime_get_boot_ns_proto;
 extern const struct bpf_func_proto bpf_ktime_get_tai_ns_proto;
 extern const struct bpf_func_proto bpf_get_current_pid_tgid_proto;
+extern const struct bpf_func_proto bpf_binprm_set_interp_proto;
 extern const struct bpf_func_proto bpf_get_current_uid_gid_proto;
 extern const struct bpf_func_proto bpf_get_current_comm_proto;
 extern const struct bpf_func_proto bpf_get_stackid_proto;
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5f..dce155c5b 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -6142,6 +6142,7 @@ union bpf_attr {
 	FN(user_ringbuf_drain, 209, ##ctx)		\
 	FN(cgrp_storage_get, 210, ##ctx)		\
 	FN(cgrp_storage_delete, 211, ##ctx)		\
+	FN(binprm_set_interp, 212, ##ctx)		\
 	/* This helper list is effectively frozen. If you are trying to	\
 	 * add a new helper, you should add a kfunc instead which has	\
 	 * less stability guarantees. See Documentation/bpf/kfuncs.rst	\
diff --git a/net/core/filter.c b/net/core/filter.c
index 2e96b4b84..187692a4a 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -8397,6 +8397,14 @@ sk_filter_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
 		return &bpf_get_socket_uid_proto;
 	case BPF_FUNC_perf_event_output:
 		return &bpf_skb_event_output_proto;
+#if IS_BUILTIN(CONFIG_BINFMT_MISC)
+	case BPF_FUNC_binprm_set_interp:
+		/*
+		 * binfmt_misc reuses SOCKET_FILTER programs to select an
+		 * interpreter; expose the helper that lets them set it.
+		 */
+		return &bpf_binprm_set_interp_proto;
+#endif
 	default:
 		return bpf_sk_base_func_proto(func_id, prog);
 	}
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* Re: [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection
  2026-07-04 21:14                         ` [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection Farid Zakaria
@ 2026-07-06 16:01                           ` Christian Brauner
  2026-07-06 16:47                             ` Farid Zakaria
  0 siblings, 1 reply; 29+ messages in thread
From: Christian Brauner @ 2026-07-06 16:01 UTC (permalink / raw)
  To: Farid Zakaria
  Cc: brauner, david.laight.linux, jack, jannh, kees, linux-fsdevel,
	linux-kernel, linux-kselftest, linux-mm, mail, shuah, viro,
	Gemini

> Historically binfmt_misc has only matched binaries by magic bytes or file
> extension, and always redirects a match to a fixed interpreter recorded in
> the registration. This is insufficient when the match requires parsing the
> binary header (e.g. inspecting ELF program headers) or when the interpreter
> must be computed per-binary rather than hard-coded.
> 
> Introduce a new 'B' (BPF) handler type. A pinned SOCKET_FILTER program is
> registered in place of the magic/mask, and no interpreter is recorded:
> 
>   echo ':name:B:<bpf_pinned_path>::::<flags>' \
>       > /proc/sys/fs/binfmt_misc/register
> 
> When a binary is executed, binfmt_misc runs the program with the
> BINPRM_BUF_SIZE file-header buffer as context. Returning 1 selects the
> handler; returning 0 falls through to the remaining handlers.
> 
> Unlike magic/extension handlers, a 'B' handler carries no interpreter of its
> own: the program chooses it via a new helper, bpf_binprm_set_interp(). This
> lets the program compute the interpreter path however it sees fit (for example
> relative to the binary). A 'B' handler is therefore a strict superset of the
> existing magic handlers -- any of them can be expressed as a program that
> matches on the header and sets a fixed interpreter.
> 
> bpf_binprm_set_interp() is exposed to SOCKET_FILTER programs and stashes the
> chosen path on a per-CPU area that binfmt_misc reads back immediately after
> the run under migrate_disable(); only a match that set an interpreter
> allocates.
> 
> Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>

Ignoring the blatant bpf abuse here I think this is quite workable. So
this can be turned into an actual design and patch imho...

Note that binfmt_misc is namespaced and can be mounted inside of user
namespace + mount namespaces fwiw. So a container mounting binfmt_misc
(a fresh instance - few do) would escape that bpf program. On the other
hand it would allow to register a custom bpf program per container if
needed...

-- 
Christian Brauner <brauner@kernel.org>

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection
  2026-07-06 16:01                           ` Christian Brauner
@ 2026-07-06 16:47                             ` Farid Zakaria
  0 siblings, 0 replies; 29+ messages in thread
From: Farid Zakaria @ 2026-07-06 16:47 UTC (permalink / raw)
  To: Christian Brauner
  Cc: david.laight.linux, jack, jannh, kees, linux-fsdevel,
	linux-kernel, linux-kselftest, linux-mm, mail, shuah, viro,
	Gemini

On Mon, Jul 6, 2026 at 9:01 AM Christian Brauner <brauner@kernel.org> wrote:
>
> > Historically binfmt_misc has only matched binaries by magic bytes or file
> > extension, and always redirects a match to a fixed interpreter recorded in
> > the registration. This is insufficient when the match requires parsing the
> > binary header (e.g. inspecting ELF program headers) or when the interpreter
> > must be computed per-binary rather than hard-coded.
> >
> > Introduce a new 'B' (BPF) handler type. A pinned SOCKET_FILTER program is
> > registered in place of the magic/mask, and no interpreter is recorded:
> >
> >   echo ':name:B:<bpf_pinned_path>::::<flags>' \
> >       > /proc/sys/fs/binfmt_misc/register
> >
> > When a binary is executed, binfmt_misc runs the program with the
> > BINPRM_BUF_SIZE file-header buffer as context. Returning 1 selects the
> > handler; returning 0 falls through to the remaining handlers.
> >
> > Unlike magic/extension handlers, a 'B' handler carries no interpreter of its
> > own: the program chooses it via a new helper, bpf_binprm_set_interp(). This
> > lets the program compute the interpreter path however it sees fit (for example
> > relative to the binary). A 'B' handler is therefore a strict superset of the
> > existing magic handlers -- any of them can be expressed as a program that
> > matches on the header and sets a fixed interpreter.
> >
> > bpf_binprm_set_interp() is exposed to SOCKET_FILTER programs and stashes the
> > chosen path on a per-CPU area that binfmt_misc reads back immediately after
> > the run under migrate_disable(); only a match that set an interpreter
> > allocates.
> >
> > Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
>
> Ignoring the blatant bpf abuse here I think this is quite workable. So
> this can be turned into an actual design and patch imho...

Glad to see you think this proposal is in the right direction.
Just for my edification, when you say "balatant bpf abuse", you mean
how it abuses the socket bpf type?
If so, do you have any guidance on a new type or should I just take a
stab at it.

For the proper RFC patch unbastardized with tests, should I start a
new thread or is
the convention to keep attaching it to this same thread?

>
> Note that binfmt_misc is namespaced and can be mounted inside of user
> namespace + mount namespaces fwiw. So a container mounting binfmt_misc
> (a fresh instance - few do) would escape that bpf program. On the other
> hand it would allow to register a custom bpf program per container if
> needed...

That makes sense -- but you will need CAP_BPF etc.. to load the new
programs though
if inside a userns.

>
> --
> Christian Brauner <brauner@kernel.org>


^ permalink raw reply	[flat|nested] 29+ messages in thread

end of thread, other threads:[~2026-07-06 16:48 UTC | newest]

Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-22  4:39 [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Farid Zakaria
2026-06-22  4:39 ` [PATCH 1/2] " Farid Zakaria
2026-06-22  9:53   ` Jori Koolstra
2026-06-23 20:14   ` Kees Cook
2026-06-23 20:35     ` Farid Zakaria
2026-06-22  4:39 ` [PATCH 2/2] selftests/exec: add test suites for $ORIGIN interpreter resolution Farid Zakaria
2026-06-22 10:39 ` [PATCH 0/2] fs: support $ORIGIN in ELF interpreter paths Jan Kara
2026-06-22 17:15   ` Farid Zakaria
2026-06-22 21:08     ` John Ericson
2026-06-25  8:50       ` Christian Brauner
2026-06-25 19:34         ` John Ericson
2026-06-26 12:39         ` Jann Horn
2026-06-26 13:26           ` David Laight
2026-06-26 13:34             ` Jann Horn
2026-06-26 13:40               ` Farid Zakaria
2026-06-26 16:28               ` David Laight
2026-06-28 12:36           ` Christian Brauner
2026-06-28 13:20             ` Christian Brauner
2026-06-28 18:44               ` David Laight
2026-06-30 16:58               ` Farid Zakaria
2026-07-02  6:47                 ` Christian Brauner
2026-06-30 17:59               ` David Laight
2026-07-02 21:42                 ` [RFC PATCH] fs: introduce pluggable ELF interpreter loader registry Farid Zakaria
2026-07-03  9:22                   ` Christian Brauner
2026-07-03 18:32                     ` Farid Zakaria
2026-07-04  8:15                       ` Christian Brauner
2026-07-04 21:14                         ` [RFC PATCH] fs: binfmt_misc: introduce eBPF-based matching and interpreter selection Farid Zakaria
2026-07-06 16:01                           ` Christian Brauner
2026-07-06 16:47                             ` Farid Zakaria

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.