Skip to content

linux-user, fix: Preserve x86 executable identity across fd changes - #400

Merged
LaurenIsACoder merged 1 commit into
lat-opensource:masterfrom
LaurenIsACoder:lauren/proc-self-exe-fd-lifetime
Aug 15, 2026
Merged

linux-user, fix: Preserve x86 executable identity across fd changes#400
LaurenIsACoder merged 1 commit into
lat-opensource:masterfrom
LaurenIsACoder:lauren/proc-self-exe-fd-lifetime

Conversation

@LaurenIsACoder

@LaurenIsACoder LaurenIsACoder commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Chromium/Electron subprocess launchers sanitize inherited file descriptors and
then re-execute the current image through /proc/self/exe. Under LATX, the
main process could start, but GPU and network-service children failed with:

LaunchProcess: failed to execvp:
/proc/self/exe

Simple /proc/self/exe tests did not reproduce the failure because they did
not perform the descriptor cleanup used by the real child-launch path.

问题

Chromium/Electron 的子进程启动器会清理继承的文件描述符,然后通过
/proc/self/exe 重新执行当前程序。在 LATX 下,主进程可以启动,但 GPU 和
网络服务子进程会失败并报告:

LaunchProcess: failed to execvp:
/proc/self/exe

简单的 /proc/self/exe 测试无法复现,因为它们没有执行真实子进程启动路径
中的文件描述符清理操作。

How the regression developed

This is a composition issue across several earlier x86 prctl changes:

  • 2345e33d785 introduced emulated x86 PR_SET_MM state, including
    prctl_mm_exe_fd, so synthetic procfs could report guest executable
    identity instead of translator identity.
  • 368ed560d8b made procfs readers duplicate that saved descriptor under the
    mmap lock, removing cross-thread read/replace races.
  • ac09a020b59 completed propagation of the emulated state across guest and
    self exec paths.
  • 858298112f3 hardened rename/unlink races by preserving the initial x86
    executable in prctl_mm_exe_fd for every translated process, rather than
    only after an explicit guest PR_SET_MM_EXE_FILE operation.

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 引入了模拟的 x86 PR_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_fd is therefore also
addressable by guest close, fcntl, dup2, and dup3 syscalls.

When a child launcher closes or overwrites that descriptor, the integer kept
in prctl_mm_exe_fd becomes stale. A later /proc/self/exe exec either fails
while duplicating the closed descriptor or operates on an unrelated descriptor
that has reused the same number. This accounts for both observed ENOENT and
EACCES outcomes.

根本原因

Linux-user 翻译使用 host 进程的文件描述符表作为 guest 文件描述符表。因此,
保存在 prctl_mm_exe_fd 中的描述符也可以被 guest 的 closefcntl
dup2dup3 系统调用直接操作。

当子进程启动器关闭或覆盖这个描述符时,prctl_mm_exe_fd 中保存的整数就会
失效。之后通过 /proc/self/exe 执行程序时,要么因为无法复制已关闭的描述符
而失败,要么误用一个复用了相同编号、但实际指向无关对象的描述符。这分别解释了
观察到的 ENOENTEACCES 结果。

Fix

Before a guest operation can destroy or mutate the descriptor currently owned
by prctl_mm_exe_fd, duplicate the executable reference with
F_DUPFD_CLOEXEC and 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:

  • close
  • clearing FD_CLOEXEC through F_SETFD
  • dup2 replacement
  • dup3 replacement

Setting an already-set FD_CLOEXEC bit does not relocate the reference. This
keeps 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/exe resolution, reserving a hard-coded fd
number, or changing guest-visible results. If preserving the reference fails,
the destructive guest operation is rejected instead of silently corrupting
translator state.

close_range is not currently implemented by this linux-user syscall path;
userspace falls back to individual closes. Any future close_range
implementation 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 语义。

保护范围包括:

  • close
  • 通过 F_SETFD 清除 FD_CLOEXEC
  • dup2 替换
  • 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:

Source i386 flags x86_64 flags
Upstream latxbuild/build-release.sh templates empty empty
AOSC latx package POCF POCF
Observed Loongnix installation empty configured C, active OC

All four characters in POCF are uppercase letters; O is capital letter O,
not zero. O supplies an already-open executable, C selects credentials
from the binary and implies O, P preserves the original argv[0], and F
pins the interpreter file at registration time. These flags affect security,
argument layout, and package-upgrade behavior; in particular, C changes
credential handling and F keeps an interpreter inode pinned until the entry
is re-registered.

This PR intentionally does not change binfmt flags. Requiring O is not a
complete 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 约定并不一致:

来源 i386 flags x86_64 flags
上游 latxbuild/build-release.sh 模板
AOSC latx 软件包 POCF POCF
实测 Loongnix 安装 配置为 C,活动项为 OC

POCF 四个字符都是大写英文字母,其中第二个字符 O 是大写字母 O,不是数字 0。
O 表示向解释器提供一个已经打开的可执行文件,C 表示根据该二进制文件选择凭据
并隐含 OP 保留原始的 argv[0]F 则在注册时固定解释器文件。这些标志会
影响安全、参数布局和软件包升级行为;
尤其是 C 会改变凭据处理方式,而 F 会持续固定解释器 inode,直到重新注册该项。

本 PR 有意不修改 binfmt flags。要求使用 O 并不能完整修复这个问题:程序启动
以后,已保存的可执行文件描述符仍与 guest 共用同一张描述符表,因此仍可能被关闭
或替换。无论初始可执行文件通过何种方式获得,翻译器都必须保证自己的内部引用有效。

后续应通过独立 PR 为各 guest ABI 定义受支持的 binfmt 约定,统一上游发布模板和
各发行版软件包,并测试 argv 保留、open-binary 处理、凭据选择、固定解释器升级和
服务重载行为。将该策略调整与本次修复分开,可以避免把正确性修复与发行版特定的
安全和生命周期决策耦合在一起。

Validation

  • Built both i386-linux-user and x86_64-linux-user translators.
  • Added freestanding x86 regressions that mutate the executable identity with
    close, F_SETFD, dup2, and dup3, then execute /proc/self/exe.
  • Confirmed the close and dup replacement cases fail on the parent revision
    and all four cases pass with this change.
  • Confirmed an Electron/Chromium application keeps its main, GPU, network,
    zygote, and renderer processes alive and reaches a ready-to-show window,
    with no /proc/self/exe launch failures or GPU/network-service crash loop.
  • git diff --check passes.

验证

  • 构建了 i386-linux-userx86_64-linux-user 两种翻译器。
  • 新增了 freestanding x86 回归测试,分别通过 closeF_SETFDdup2
    dup3 修改可执行文件身份描述符,然后执行 /proc/self/exe
  • 确认 close 和 dup 替换用例在父版本上失败,而本次修改后的四个用例全部通过。
  • 确认 Electron/Chromium 应用的主进程、GPU、网络、zygote 和 renderer 进程
    均能持续运行并到达 ready-to-show 窗口,且没有 /proc/self/exe 启动失败或
    GPU/网络服务崩溃循环。
  • git diff --check 通过。

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>
@LaurenIsACoder
LaurenIsACoder marked this pull request as ready for review August 15, 2026 03:01
@LaurenIsACoder
LaurenIsACoder merged commit 875508c into lat-opensource:master Aug 15, 2026
20 checks passed
@LaurenIsACoder
LaurenIsACoder deleted the lauren/proc-self-exe-fd-lifetime branch August 15, 2026 03:01
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