Re: [PATCH 1/2] fs: support $ORIGIN in ELF interpreter paths

From: Kees Cook

Date: Tue Jun 23 2026 - 16:15:21 EST


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