linux-user, fix: Preserve x86 executable identity across fd changes - #400
Merged
LaurenIsACoder merged 1 commit intoAug 15, 2026
Conversation
The x86 prctl emulation keeps an internal descriptor for the guest executable so /proc/self/exe and PR_SET_MM_EXE_FILE can retain guest identity. That descriptor shares the host fd table with guest syscalls, so Chromium and Electron child setup can close or overwrite it before re-executing /proc/self/exe. Relocate the internal close-on-exec reference before guest close, F_SETFD clearing, dup2, or dup3 operations mutate its descriptor number. The guest operation still applies to the old number, while translator state keeps a valid reference. Add x86 regressions for close, descriptor-flag changes, and dup replacement followed by /proc/self/exe execution. Signed-off-by: Hanlu Li <heuleehanlu@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Chromium/Electron subprocess launchers sanitize inherited file descriptors and
then re-execute the current image through
/proc/self/exe. Under LATX, themain process could start, but GPU and network-service children failed with:
Simple
/proc/self/exetests did not reproduce the failure because they didnot perform the descriptor cleanup used by the real child-launch path.
问题
Chromium/Electron 的子进程启动器会清理继承的文件描述符,然后通过
/proc/self/exe重新执行当前程序。在 LATX 下,主进程可以启动,但 GPU 和网络服务子进程会失败并报告:
简单的
/proc/self/exe测试无法复现,因为它们没有执行真实子进程启动路径中的文件描述符清理操作。
How the regression developed
This is a composition issue across several earlier x86 prctl changes:
2345e33d785introduced emulated x86PR_SET_MMstate, includingprctl_mm_exe_fd, so synthetic procfs could report guest executableidentity instead of translator identity.
368ed560d8bmade procfs readers duplicate that saved descriptor under themmap lock, removing cross-thread read/replace races.
ac09a020b59completed propagation of the emulated state across guest andself exec paths.
858298112f3hardened rename/unlink races by preserving the initial x86executable in
prctl_mm_exe_fdfor every translated process, rather thanonly after an explicit guest
PR_SET_MM_EXE_FILEoperation.Those changes made executable identity stable for procfs readers, but they did
not establish an ownership boundary between the translator's saved descriptor
and guest descriptor operations.
问题如何形成
这是此前多项 x86 prctl 改动组合后产生的问题:
2345e33d785引入了模拟的 x86PR_SET_MM状态,其中包括prctl_mm_exe_fd,使合成 procfs 能够报告 guest 可执行文件身份,而不是翻译器自身的身份。
368ed560d8b让 procfs 读取方在 mmap 锁保护下复制已保存的描述符,消除了跨线程读取和替换之间的竞争。
ac09a020b59补全了模拟状态在 guest exec 和 self exec 路径之间的传递。858298112f3为每个被翻译的 x86 进程都在prctl_mm_exe_fd中保存初始可执行文件,而不再仅限于 guest 显式执行
PR_SET_MM_EXE_FILE之后,从而加固了可执行文件被重命名或删除时的竞争问题。
这些改动保证了 procfs 读取方看到的可执行文件身份稳定,但没有在翻译器保存的
描述符与 guest 文件描述符操作之间建立所有权边界。
Root cause
Linux-user translation uses the host process descriptor table as the guest
descriptor table. The descriptor stored in
prctl_mm_exe_fdis therefore alsoaddressable by guest
close,fcntl,dup2, anddup3syscalls.When a child launcher closes or overwrites that descriptor, the integer kept
in
prctl_mm_exe_fdbecomes stale. A later/proc/self/exeexec either failswhile duplicating the closed descriptor or operates on an unrelated descriptor
that has reused the same number. This accounts for both observed
ENOENTandEACCESoutcomes.根本原因
Linux-user 翻译使用 host 进程的文件描述符表作为 guest 文件描述符表。因此,
保存在
prctl_mm_exe_fd中的描述符也可以被 guest 的close、fcntl、dup2和dup3系统调用直接操作。当子进程启动器关闭或覆盖这个描述符时,
prctl_mm_exe_fd中保存的整数就会失效。之后通过
/proc/self/exe执行程序时,要么因为无法复制已关闭的描述符而失败,要么误用一个复用了相同编号、但实际指向无关对象的描述符。这分别解释了
观察到的
ENOENT和EACCES结果。Fix
Before a guest operation can destroy or mutate the descriptor currently owned
by
prctl_mm_exe_fd, duplicate the executable reference withF_DUPFD_CLOEXECand atomically publish the replacement under the mmap lock.The requested guest operation then continues against the original descriptor
number, preserving its normal semantics.
The protection covers:
closeFD_CLOEXECthroughF_SETFDdup2replacementdup3replacementSetting an already-set
FD_CLOEXECbit does not relocate the reference. Thiskeeps the common descriptor-sanitization path free of unnecessary duplicate
executable references.
This location is the narrowest ownership boundary: it protects the internal
reference at the syscall that would invalidate it, without special-casing
Chromium, changing
/proc/self/exeresolution, reserving a hard-coded fdnumber, or changing guest-visible results. If preserving the reference fails,
the destructive guest operation is rejected instead of silently corrupting
translator state.
close_rangeis not currently implemented by this linux-user syscall path;userspace falls back to individual closes. Any future
close_rangeimplementation must use the same ownership helper when its range contains the
saved executable descriptor.
修复方案
在 guest 操作可能销毁或修改
prctl_mm_exe_fd当前持有的描述符之前,先使用F_DUPFD_CLOEXEC复制可执行文件引用,并在 mmap 锁保护下原子发布替代引用。然后让 guest 请求的操作继续作用于原来的描述符编号,从而保持正常的 guest 语义。
保护范围包括:
closeF_SETFD清除FD_CLOEXECdup2替换dup3替换如果
FD_CLOEXEC已经设置,再次设置它不会搬移内部引用。这样可以避免在常见的描述符清理路径上产生不必要的可执行文件引用副本。
这个位置是最小且最明确的所有权边界:在真正会使内部引用失效的系统调用入口保护
它,不需要对 Chromium 做特殊处理,不需要改变
/proc/self/exe的解析方式,不需要保留一个硬编码的 fd 编号,也不会改变 guest 可见的操作结果。如果无法保存
内部引用,则拒绝该破坏性 guest 操作,而不是静默损坏翻译器状态。
当前 linux-user 系统调用路径尚未实现
close_range,userspace 会回退到逐个关闭描述符。未来如果实现
close_range,当其范围包含已保存的可执行文件描述符时,也必须使用相同的所有权保护函数。
binfmt registration differences
The investigation also found that current LATX installations do not share one
binfmt contract:
latxbuild/build-release.shtemplateslatxpackagePOCFPOCFC, activeOCAll four characters in
POCFare uppercase letters;Ois capital letter O,not zero.
Osupplies an already-open executable,Cselects credentialsfrom the binary and implies
O,Ppreserves the originalargv[0], andFpins the interpreter file at registration time. These flags affect security,
argument layout, and package-upgrade behavior; in particular,
Cchangescredential handling and
Fkeeps an interpreter inode pinned until the entryis re-registered.
This PR intentionally does not change binfmt flags. Requiring
Ois not acomplete repair for this bug: after startup, the saved executable descriptor
still shares the guest descriptor table and can still be closed or replaced.
The translator must keep its internal reference valid regardless of how the
initial executable was obtained.
A separate follow-up PR should define a supported binfmt contract for each
guest ABI, align the upstream release templates and downstream packages, and
test argv preservation, open-binary handling, credential selection, pinned
interpreter upgrades, and service reload behavior. Keeping that policy change
separate avoids coupling a correctness fix to distribution-specific security
and lifecycle decisions.
binfmt 注册差异
调查还发现,当前不同 LATX 安装使用的 binfmt 约定并不一致:
latxbuild/build-release.sh模板latx软件包POCFPOCFC,活动项为OCPOCF四个字符都是大写英文字母,其中第二个字符O是大写字母 O,不是数字 0。O表示向解释器提供一个已经打开的可执行文件,C表示根据该二进制文件选择凭据并隐含
O,P保留原始的argv[0],F则在注册时固定解释器文件。这些标志会影响安全、参数布局和软件包升级行为;
尤其是
C会改变凭据处理方式,而F会持续固定解释器 inode,直到重新注册该项。本 PR 有意不修改 binfmt flags。要求使用
O并不能完整修复这个问题:程序启动以后,已保存的可执行文件描述符仍与 guest 共用同一张描述符表,因此仍可能被关闭
或替换。无论初始可执行文件通过何种方式获得,翻译器都必须保证自己的内部引用有效。
后续应通过独立 PR 为各 guest ABI 定义受支持的 binfmt 约定,统一上游发布模板和
各发行版软件包,并测试 argv 保留、open-binary 处理、凭据选择、固定解释器升级和
服务重载行为。将该策略调整与本次修复分开,可以避免把正确性修复与发行版特定的
安全和生命周期决策耦合在一起。
Validation
i386-linux-userandx86_64-linux-usertranslators.close,F_SETFD,dup2, anddup3, then execute/proc/self/exe.and all four cases pass with this change.
zygote, and renderer processes alive and reaches a ready-to-show window,
with no
/proc/self/exelaunch failures or GPU/network-service crash loop.git diff --checkpasses.验证
i386-linux-user和x86_64-linux-user两种翻译器。close、F_SETFD、dup2和
dup3修改可执行文件身份描述符,然后执行/proc/self/exe。均能持续运行并到达 ready-to-show 窗口,且没有
/proc/self/exe启动失败或GPU/网络服务崩溃循环。
git diff --check通过。