Skip to content

feat(elfpatch): stamp DT_RPATH on executables — DT_RUNPATH cannot reach what they dlopen (0.0.57) - #41

Merged
Sunrisepeak merged 1 commit into
mainfrom
feat/force-rpath-executables
Aug 10, 2026
Merged

feat(elfpatch): stamp DT_RPATH on executables — DT_RUNPATH cannot reach what they dlopen (0.0.57)#41
Sunrisepeak merged 1 commit into
mainfrom
feat/force-rpath-executables

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

patchelf --set-rpath 默认写 DT_RUNPATH。DT_RUNPATH 只对携带它的对象生效;
DT_RPATH 对进程内任意深度的 dlopen 都生效

图形栈的加载链有三到四层 dlopen(glvnd → vendor → EGL 外部平台模块 → 它自己的依赖),
只有传递性标签能到底。

实测

同样的路径内容,只改标签类型,无环境变量、索引零改动、interposer 零改动:

可执行文件标签 egl gles2 egl-surfaceless glx
DT_RPATH NVIDIA RTX 4080 NVIDIA NVIDIA NVIDIA
DT_RUNPATH llvmpipe llvmpipe llvmpipe NVIDIA

真机上抽样 73 个已安装可执行文件:1 个 DT_RPATH,68 个 DT_RUNPATH,其中 55 个路径里
已经有 subos farm
。唯一那个是 godot —— 这套栈里唯一被观察到图形真能用的程序 ——
因为它的 recipe 自己调 patchelf --force-rpath 并写明了原因。那份知识没有任何机制
能到达另外 68 个包。

只改可执行文件,这是关键的一半

上强制 DT_RPATH 是实测有害的(xim-pkgindex#593):传递性向下也成立,
库的 RPATH 会进入它下方每一次查找,NVIDIA 的 EGL interposer 被这样打标签后
eglInitialize 直接失败。

PT_INTERP 是判据 —— 而它本来就在被计算(用于决定是否 --set-interpreter),
现在算一次两处用,在有 loader 的路径上零成本。

标志经由一个 _rpath_flags() 而不是在两个调用点各写一遍 —— 逐点决定正是这件事
一开始变成逐包决定的原因

行为变更(明说)

DT_RPATH 优先级高于 LD_LIBRARY_PATH,DT_RUNPATH 低于它。所以用户不能再用
LD_LIBRARY_PATH 覆盖这些载荷。

实测:subos 里 LD_LIBRARY_PATH 根本没被设置,而 recipe 自己的注释把设计目标写成
"what makes the stack resolve without anyone setting LD_LIBRARY_PATH" —— 所以今天
没有任何东西依赖它
。DT_RPATH 也不传播给子进程,这正是它在这里安全的原因。

已装载荷保持旧标签直到重装;本改动在安装时打标签。

测试

录制式 patchelf 桩按文件名回答 --print-interpreter,驱动真实谓词,断言标志落在
可执行文件上而落在库上。双向证伪后才采信:完全不打标签 → 红;库也打 → 红。

设计:xlings/.agents/docs/2026-08-10-graphics-stack-design.md §E1a
关联:openxlings/xlings#532 #534

…ch what they dlopen (0.0.57)

`patchelf --set-rpath` writes DT_RUNPATH by default. DT_RUNPATH is consulted
only for the object that carries it; DT_RPATH is consulted for every dlopen
anywhere in the process, however deep.

The graphics stack's load chain is three to four dlopens deep — glvnd dlopens
a vendor, the vendor dlopens its EGL external platform modules, those dlopen
their own dependencies — so only the transitive tag reaches the bottom.
Measured on an NVIDIA 550.144.03 host: identical path content, stamped
DT_RPATH, puts GLX, EGL, GLESv2 and headless surfaceless EGL all on the GPU
with no environment variable, no index change and no change to the interposer;
stamped DT_RUNPATH it renders on llvmpipe.

The path was always right and the tag was always wrong. Of 73 installed
executables on a real home: 1 carried DT_RPATH, 68 carried DT_RUNPATH, and 55
of those 68 already had the subos farm on that path. The single DT_RPATH
binary was godot — the only program in the stack ever observed to render on
the GPU — because its recipe calls `patchelf --force-rpath` by hand and
documents exactly why. That knowledge had no way to reach the other 68.

EXECUTABLES ONLY, and that is the load-bearing half. Forcing DT_RPATH on a
LIBRARY is measured harmful (xim-pkgindex#593): transitivity runs downward
too, so a library's RPATH enters every lookup made beneath it, and the NVIDIA
EGL interposer stamped that way fails eglInitialize outright. PT_INTERP is the
predicate, and it is the same one that already decides whether to set an
interpreter — computed once now and used for both, so it costs nothing where a
loader is set.

The flag goes through one `_rpath_flags()` rather than being inlined at the
two call sites, because a per-site decision is how this became a per-package
decision in the first place.

Behaviour change worth stating plainly: DT_RPATH outranks LD_LIBRARY_PATH
while DT_RUNPATH loses to it, so a user can no longer override these payloads
with LD_LIBRARY_PATH. Measured: LD_LIBRARY_PATH is not set inside a subos at
all, and the recipes' own comments state the design goal as "what makes the
stack resolve without anyone setting LD_LIBRARY_PATH" — so nothing that exists
today relies on it. DT_RPATH also does not propagate to child processes, which
is precisely why it is the safe mechanism here.

Existing payloads keep their old tag until reinstalled; this stamps at install
time. Design: xlings/.agents/docs/2026-08-10-graphics-stack-design.md §E1a.

Test drives the real predicate through a recording patchelf stub that answers
--print-interpreter by filename, and asserts the flag lands on the executable
and NOT on the library. Falsified both ways before being trusted: never
stamping fails it, and stamping libraries too fails it.
@Sunrisepeak
Sunrisepeak merged commit 385a1e9 into main Aug 10, 2026
1 check passed
Sunrisepeak added a commit to mcpplibs/mcpp-index that referenced this pull request Aug 10, 2026
elfpatch now stamps DT_RPATH on executables instead of DT_RUNPATH.

DT_RUNPATH is consulted only for the object carrying it; DT_RPATH is consulted
for every dlopen anywhere in the process. The graphics stack's load chain is
three to four dlopens deep, so only the transitive tag reaches the bottom —
measured, the same path content stamped DT_RPATH puts GLX, EGL, GLESv2 and
headless surfaceless EGL all on the GPU, and stamped DT_RUNPATH renders on
llvmpipe.

Executables only. Forcing DT_RPATH on a library is measured harmful
(xim-pkgindex#593) because transitivity runs downward too.

All THREE platform sections, verified by PARSING the recipe rather than
reading the diff — a regex edit to a .lua applies cleanly and can mean
something else:

  linux    0.0.57=yes  sha=7b24c2f156ce  versions=18
  macosx   0.0.57=yes  sha=7b24c2f156ce  versions=18
  windows  0.0.57=yes  sha=7b24c2f156ce  versions=18

CN mirror uploaded and verified by SIZE (gitcode returns 200 with an HTML
landing page for an asset that is not there, so the status code proves
nothing): 147212 bytes, matching the GitHub archive.

Upstream: openxlings/libxpkg#41
Sunrisepeak added a commit to openxlings/xim-pkgindex that referenced this pull request Aug 10, 2026
The loader tag now has one owner. elfpatch (libxpkg 0.0.57) stamps DT_RPATH on
executables instead of DT_RUNPATH, and closure_check gains rule E to say so at
install time on the user's machine when a payload arrives with the old tag.

DT_RUNPATH is consulted only for the object carrying it; DT_RPATH is consulted
for every dlopen anywhere in the process. A GL program reaches its driver
through three levels of dlopen, so the tag — not the path — decided whether it
rendered on the GPU. Of 73 installed executables on a real home, 1 carried
DT_RPATH and 55 of the other 68 already carried the correct path.

Also: `subos info` reports host driver drift (the detector existed as
`xlings-gl-doctor` and lacked a channel), and `subos use --sandbox` without
`--gpu` says the sandbox exposes no GPU — under three conditions, so it can
only fire where acting on it changes the outcome.

All THREE platform sections, verified by PARSING rather than reading the diff:

  linux    latest=2026.8.10.4  arch=[aarch64,x86_64]
  macosx   latest=2026.8.10.4  arch=[aarch64]
  windows  latest=2026.8.10.4  arch=[x86_64]

Hashes taken from the assets in xlings-res — what `XLINGS_RES` actually
resolves to — not from the upstream release they were mirrored from.

CN mirror uploaded from a local shell and verified by SIZE on all four
platforms; before the upload all four returned 127 bytes, so the status code
would have said 200 for assets that were not there.

Upstream: openxlings/xlings#536, openxlings/libxpkg#41, mcpplibs/mcpp-index#202

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant