* [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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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 1 sibling, 1 reply; 16+ 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] 16+ 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 0 siblings, 1 reply; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ messages in thread
end of thread, other threads:[~2026-06-26 16:28 UTC | newest] Thread overview: 16+ 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
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.