Linux 内核将支持 $ORIGIN,某种程度上。
Linux kernel will support $ORIGIN, sort of

原始链接: https://fzakaria.com/2026/07/20/linux-kernel-will-support-origin-sort-of

在 2026 年的 TacoSprint 活动中,作者提议通过在 `PT_INTERP` 段中增加对 `$ORIGIN` 的内核级支持,以实现 Nix 中的可重定位二进制文件。作者本预想会遭到反对,结果却与 Linux VFS 维护者 Christian Brauner 进行了富有成效的合作。 他们没有采用简单的补丁,而是利用 eBPF 和 `binfmt_misc` 开发了一套解决方案。通过将解释器选择卸载到可编程的 eBPF 内核模块,系统能够动态解析路径,从而有效地支持可重定位二进制文件。 一项关键的突破是增加了新的 `binfmt_misc` 分发模式(“L 标志”),该模式允许内核在原生执行二进制文件的同时简单地覆盖解释器。这避免了通常困扰解释器切换的标识问题,例如 `argv[0]` 或 `/proc/self/exe` 解析错误。 展望未来,作者计划引入一个利用自定义 `PT_INTERP_NIX` 段的 NixOS 模块。这种方法确保了向后兼容性,仅允许选择加入的二进制文件获得可重定位能力。这一进展显著推动了 Nix 生态系统的边界,使其向真正可移植、与环境无关的二进制文件迈进了一步。

这段 Hacker News 的讨论探讨了一项拟议的 Linux 内核变更,旨在为可执行加载器(executable loaders)和 shebang 行引入对 `$ORIGIN` 的支持。 从历史上看,尽管动态链接器(`ld.so`)支持使用 `$ORIGIN`(代表可执行文件所在的目录)进行依赖解析,但内核本身要求解释器(`PT_INTERP`)必须使用绝对路径。这迫使二进制文件必须硬编码指向 `/lib64/ld-linux-x86-64.so.2` 等系统加载器的路径,从而阻碍了可移植性。 支持者认为,内核级对 `$ORIGIN` 的支持将允许二进制文件携带自己的加载器,从而实现更具可移植性、自包含的应用程序——这也是 Nix 和各种应用打包工具长期追求的目标。然而,讨论中也出现了对此方案的怀疑声音。批评者指出,为此目的修改内核可能显得“怪异”或不必要,并建议通过更好的加载器委托或元数据管理在用户空间解决此问题。此外,一些参与者警告称,将 Bazel 等构建系统的模式引入内核,可能是在移植“缺陷”而非采纳最优的架构方案,这引发了关于系统级兼容性与专业化二进制分发之间权衡的辩论。
相关文章

原文

For some reason, during TacoSprint 2026 I decided to see if we could tackle relocatable binaries in Nix.

I enjoy these lofty goals to push Nix and the surrounding ecosystem forward. I am bold if not stupid.

I left the last earlier post with one potential idea of how to get there:

We could patch the Linux kernel so that $ORIGIN is supported in PT_INTERP and the shebang.

I waded through the complexity of sending patches over email (turns out I actually enjoy this workflow!), and sent a proposal to the Linux kernel mailing list.

My first attempt here proposed simply adding direct support for $ORIGIN in the Virtual File System (VFS) subsystem.

I waited nervously. I was expecting the result from what I had come to read about online; someone non-politely telling me to F$#CK OFF because there is something I missed, misunderstood or did not consider. 🤬

The result was completely different. 😲

Christian Brauner, the maintainer for VFS responded to me in good faith, asking for the rationale for the change and eventually proposing some ways in which such a support could make it into the subsystem.

Note It definitely helped having someone like John Ericson chime in and advocate why having a non-fixed interpreter (PT_INTERP) is useful to Nix and other use-cases (i.e. Buck & Bazel).

He offered that potentially we could leverage eBPF as a programmable way to select an interpreter through binfmt_misc.

Whoa! 🤯

I wanted to merely allow $ORIGIN but a programmable selection could let us do anything!

The idea must have really intrigued him because soon-after, on his vacation, Christian offered the first draft of such a solution. We went back and forth a little over the mailing list and the end result is a patch series that will make its way into -next branch in the near future.

If you don’t know what eBPF is or binfmt_misc, WTF did we just collaborate on?

Let’s take a look!

I won’t do eBPF justice, and there are plenty of articles online about it as it’s quite in-vogue at the moment.

tl;dr; You can write programs in a C subset that gets compiled to an instruction set whose virtual machine is running within the kernel. Shouldn’t the kernel be super fast? Yes, the programs are jitted to their native CPU architecture and the programs have a fixed-time slice. Isn’t this some crazy vulnerability for the kernel? Before any code is loaded it is “verified” to be safe. Checkout this guide for more info.

We can now support $ORIGIN with a relatively simple eBPF program:

SEC("struct_ops.s/match")
bool BPF_PROG(nix_match, struct linux_binprm *bprm)
{
  return !bpf_strncmp(bprm->buf, 4, "\x7f" "ELF");
}

SEC("struct_ops.s/load")
int BPF_PROG(nix_load, struct linux_binprm *bprm)
{
  char path[256];
  long n;

  n = bpf_path_d_path(&bprm->file->f_path, path, sizeof(path));
  if (n < 0)
    return n;

  /* derive the loader location from the binary's path */

  return bpf_binprm_set_interp(bprm, path, sizeof(path));
}

SEC(".struct_ops.link")
struct binfmt_misc_ops nix = {
  .match = (void *)nix_match,
  .load = (void *)nix_load,
  .name = "nix",
};

Once the above program is loaded and registered into the kernel, we then ask the binfmt_misc subsystem to trigger it. Checkout this thread if you want to see the complete example.

> bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
> echo ':origin:B::::nix:' > /proc/sys/fs/binfmt_misc/register

What does that mean?

It means that every binary now triggers the nix_match function above, in this case any ELF file, but it could be executables with a new segment like PT_INTERP_NIX, and the kernel will ask nix_load to determine the interpreter to use dynamically.

Our special BPF program has support for $ORIGIN 💥

What else could you do?

Well we can now even completely replace the traditional QEMU binfmt_misc registration script with a BPF program now like this one.

What else can we do?

Since we can now programmatically select our interpreter based on anything in the file, we can do quite a lot. I’m keen to hear your suggestions and ideas 💡.

Some of the smaller items are that we can even support $ORIGIN in the shebangs (#!$ORIGIN/bin/ld.so) very easily as seen here: we simply look at the first 256 bytes of the file and look for $ORIGIN to trigger.

One downside or side-effect of the traditional binfmt_misc hand-off was that the way in which the desired final binary was invoked was non-transparent.

The registered interpreter becomes the process. It owns the entire process identity, and the binary you actually asked to run gets demoted to an argument. For wine or qemu that’s acceptable as they are emulators but for a per-binary BPF loader that might pick a traditional ld.so it does not make much sense.

This leaks in a few painful ways but the simplest are :

  • argv[0] and /proc/<pid>/cmdline show the interpreter invocation, not what you executed.
  • /proc/self/exe names the interpreter. Relocatable programs commonly locate themselves through /proc/self/exe, and instead they find the dynamic linker. 😩

Christian sent a large patch series for this as well. His latest patch series adds two new dispatch modes that close the gap from opposite ends and covers a few other gotchas that these modes can fix.

The loader substitition L is the one I’m most excited about for Nix.

With the L flag, the kernel executes the matched binary natively as the main image, and merely substitutes the registered interpreter for the loader named in the binary’s PT_INTERP. binfmt_misc stops being a hand-off and becomes a plain PT_INTERP override. There’s no contract and no identity to reconstruct, so a stock dynamic loader works unchanged.

Where does this leaves us?

I’ll be tracking the Linux kernel releases and, once this lands in -next and ships in a tagged release, I plan to upstream a NixOS module that registers the $ORIGIN support at boot. 🎉

The plan is to gate it on a new PT_INTERP_NIX segment rather than matching every ELF file. That keeps things backwards compatible: the BPF handler only kicks in for binaries that explicitly opt-in by carrying the new segment. This means Nix produced binaries continue to work without the BFP handler but those that have it may elevate themselves to relocatable status.

A ship in harbor is safe, but that is not what ships are built for. — John A. Shedd

联系我们 contact @ memedata.com