cua-driver Linux X11 MPX 孤儿设备回收:基于版本化属主的 master 指针/键盘对自动恢复机制
【免费下载链接】cuaScale computer-use 2.0 with open-source drivers, cross-OS fleets, and benchmarks for training, evaluation, and data generation.项目地址: https://gitcode.com/GitHub_Trending/cua/cua
本篇技术指南聚焦 cua-driver(Linux 端 X11 背景输入注入与 MPX 真实指针路径)在进程异常退出后遗留的 XInput master 设备对(master pointer/keyboard pair)问题,完整讲解其"版本化属主(versioned ownership)"恢复契约、reap_orphaned_master_pointers的源码实现,以及如何在真实 Xorg 桌面上通过聚焦回归门禁 linux-mpx-recovery.py 复现与验证整个生命周期。读完你将掌握:为什么裸 PID 不足以安全回收设备、恢复为何只发生在独立守护进程启动时、以及如何产出可供 CI 归档的proof.json证据。
问题背景:无焦点窃取输入路径留下的孤儿设备
cua-driver 在 Linux X11 上实现"无焦点窃取(no focus steal)"输入,依赖 XInput2(XI2)的 MPX 机制:驱动通过XIAddMaster创建一个临时 master pointer/keyboard 对,再经由/dev/uinput创建真实指针从设备(slave pointer)挂到该 master 下,配合设备级 shield grab 将拖动事件送达目标窗口而不触发窗口管理器抢焦点。这条路径的完整实现位于 platform-linux/src/input/mod.rs。
正常情况下,每次输入操作结束都会执行forget_master_pointer,通过XIRemoveMaster回收设备。但文档(关联文档 linux-mpx-recovery.md,对应 issue #3337)记录了一个现实缺陷:
在真实 Xorg 21.1.11 桌面 + libinput +
/dev/uinput环境下,正常的背景滚动操作能正确投递并移除临时 master 设备;但当滚动操作持续较长时间、进程被 SIGTERM 中断时,master pointer/keyboard 对会在属主进程退出后残留下来,成为 X 服务器上的孤儿设备。
文档同时指出,该环境下 GTK 键盘 fixture 在回收前仍能正常接收按键,因此它并未复现 issue 中单独报告的 Chrome 键盘症状——即孤儿设备的表现随客户端而异,必须从设备生命周期层面系统性解决,而不是针对某个应用打补丁。
为什么"解析旧设备名里的 PID"不安全
初看最直接的方案是:从遗留的 master 设备名中解析出 PID,若进程不存在则删除设备。但文档明确否决了这一方案,因为回收逻辑必须区分以下几种难以仅凭 PID 区分的情形:
| 情形 | 说明 |
|---|---|
| 属主进程存活(live peer) | 设备仍被正在运行的驱动会话使用,绝不能回收 |
| PID 复用(PID reuse) | 旧进程已死,但 PID 被新进程占用,解析到相同 PID 可能误判为存活或误删新进程的设备 |
| 进程元数据不可访问 | hidepid挂载、权限不足会导致/proc/{pid}条目不可读,缺失条目不等于进程不存在 |
| 不同主机或 PID 命名空间 | 多个主机/容器共享同一个 X server 时,命名空间内的 PID 不具备全局唯一性 |
| 旧版/未知属主设备 | 旧版本遗留的CUA legacy mp-123-1一类设备名,无法安全解析属主 |
因此文档给出的原则是:从旧设备名中裸解析 PID 不足以判定属主,未知或旧版属主的设备必须保持原样,由人工在核实属主后手动回收,自动恢复绝不触碰它们。
恢复契约:把属主编码进 XIAddMaster 的设备名
版本化属主名格式
文档定义的新版 master 设备名编码了一个"版本化属主域(versioned ownership domain)",包含五个要素:
- 属主域(domain):由内核启动标识(
/proc/sys/kernel/random/boot_id)、PID 命名空间(/proc/self/ns/pid)与有效用户(geteuid)经 SHA-256 派生出的 16 字节哈希; - PID:属主进程 ID(4 字节);
- 进程启动时刻(start time):取自
/proc/{pid}/stat的starttime字段(8 字节),用于识别 PID 复用; - 唯一 nonce:每次创建递增的 8 字节计数器;
- 设备角色后缀:
pointer/keyboard/uinput pointer。
实际生成的名称形如(源码 mpx_owner.rs):
CUA v1.{domain_b64url}.{pid(4B)+start_ticks(8B)_b64url}.{nonce_b64url}编码采用 URL-safe 无填充 base64。其中pid + start_ticks打包为 12 字节,starttime是内核 jiffies 计数,同一 PID 若被复用,新进程的starttime必然不同,从而可靠识别 PID 复用。
所有权随 XIAddMaster 原子写入
文档特别强调:属主信息是XIAddMaster调用参数的一部分(即直接写入 master 设备名),而不是先创建设备、再通过独立的 XI2 property 写入。这样 SIGKILL 无法打断"设备创建与属主标记"之间的窗口——不存在未标记属主的新 master 残留的可能。对应源码在 input/mod.rs 的ensure_master_pointer_for_session:先创建 uinput 从设备(失败则不触碰 X 层级),再以含属主名的XIAddMaster创建 master 对,最后挂接 slave。
恢复的触发与执行条件
- 只在独立守护进程(standalone daemon)启动时执行恢复:
run_serve在绑定 Unix socket 前调用recover_orphaned_mpx_devices()(见 serve.rs); - 只回收"属主被证明确实不存在"或"PID 已被复用"的匹配设备对;
- 设备 ID 校验与移除必须处于同一个 X server grab 下(
XGrabServer),防止枚举与移除之间设备 ID 被其他 X 客户端复用。
明确不执行恢复的情形
- 嵌入式 SDK 直接启动不执行恢复(
recover_orphaned_mpx_devices的注释明确要求其不参与 registry 构造,只读命令如describe/list-tools绝不能变更 X11 设备,见 platform-linux/src/lib.rs); - Wayland 行为完全不变(恢复函数在检测到
WAYLAND_DISPLAY时直接返回); - SIGTERM/SIGKILL 中断遗留的设备会一直保留,直到下一个独立守护进程启动。
源码级深入:属主判定与回收的实现细节
属主域构造(mpx_owner.rs)
Owner::current()构造当前进程属主时做了两道命名空间自检:
- 解析
/proc/self/stat得到首字段 PID,若与std::process::id()不一致,说明 procfs 暴露的是另一个 PID 命名空间,直接报错; - 再读取
/proc/{self_pid}/stat的starttime与自身比对,不一致同样报错("procfs does not identify this PID namespace")。
随后以boot_id、PID 命名空间 inode 与geteuid拼接后 SHA-256,取前 16 字节作为domain。这三个输入共同界定了"同一内核启动 + 同一 PID 命名空间 + 同一有效用户"的属主域,使跨主机/跨容器共享 X server 时互不误伤。
starttime的解析(start_ticks)还处理了一个经典陷阱:/proc/{pid}/stat的comm字段可能包含空格与右括号,必须用rsplit_once(')')取最后一个)之后的字段,再取第 20 个空白分隔字段(starttime 是整体第 22 字段,即 tail 的索引 19)。该解析有专门单测覆盖name with ) parentheses这类畸形 comm(mpx_owner.rs)。
名称解析的严格性(from_pointer_name,mpx_owner.rs)
- 必须以
CUA v1.开头、pointer结尾,且三段均为合法 URL-safe base64 且长度精确(16/12/8 字节); - PID 必须满足
0 < pid <= i32::MAX; - 拒绝任何额外后缀:单测验证了
keyboard、XTEST pointer、pointer extra、CUA legacy mp-123-1 pointer、CUA v1.bad.bad.bad pointer、ordinary pointer全部返回None(mpx_owner.rs); - 名称长度受 uinput 约束(
UINPUT_MAX_NAME_SIZE80 字节、evdev 断言留 1 字节,实际可用 78 字节),单测确认即使 PID 取i32::MAX、start_ticks取u64::MAX,完整名称仍不超限。
过期判定(stale_in,mpx_owner.rs)
判定顺序设计为"失败保守"(fail-closed,宁可保留):
domain不一致 → 非过期(外来属主域,直接保留);/proc/{pid}/stat可读且starttime与设备名记录的不同 →过期(PID 已被复用);- 读取返回
NotFound→ 要求内核独立报告kill(pid, 0)返回ESRCH才算过期。之所以额外要求ESRCH,是因为hidepid或权限不足会让/proc条目"看起来不存在",只有内核明确说"没有这个进程"才可信; - 其他错误(
PermissionDenied、InvalidData、Interrupted)→ 一律视为未过期。
单测矩阵印证了这一契约:live_peer_and_unverifiable_metadata_are_preserved(存活属主、元数据不可读、外来 domain 均保留)与 dead_owner_and_reused_pid_are_stale(ESRCH 缺席、starttime 变化均为过期)。
未知属主时的旧版回退(input/mod.rs)
若Owner::current()失败(procfs 身份无法确认),master_pointer_name回退到旧版命名CUA {cursor} mp-{pid}-{nonce}。该回退刻意保持旧版清理行为——未知 procfs 身份的设备不能参与自动恢复,因为恢复方无法安全判定其是否过期。
回收主流程(input/mod.rs)
reap_orphaned_master_pointers的执行序列:
- 存在
WAYLAND_DISPLAY→ 直接返回; Owner::current()失败 → 直接返回(不冒险);XGrabServer抢占服务器(grab 期间只做本地 procfs 读取与kill(pid, 0)探测,不涉及网络或任意文件系统读取);XIQueryDevice枚举全部设备,仅处理XIMasterPointer类型;- 逐个用
Owner::from_pointer_name解析名称,stale_in(&owner)为真则调用remove_master_pointer(XIRemoveMaster,return_mode = XIAttachToMaster归还到 Virtual core pointer/keyboard,见 input/mod.rs),并记录 tracing 日志; XUngrabServer+XSync,出错仅告警不中断。
该函数仅由独立守护进程启动路径调用(serve.rs),与 platform-linux/src/lib.rs 中"read-only 命令不得变更 X11 设备"的约束严格对应。
在真实 Xorg 上复现聚焦原生门禁
环境前提
文档强调,本门禁必须使用一次性(disposable)真实 Xorg 桌面,且满足:
- libinput 驱动可用;
/dev/uinput可读可写(guest 本地 uinput 访问);- 安装
xinput与 Python GTK3 绑定,且存在会话总线(session bus)。
Xvfb 与 TigerVNC 不支持本测试所需的设备热插拔路径——Xvfb 没有 udev/libinput 热插拔,uinput 设备永远不会成为 X 输入 slave;Xtigervnc 只暴露内建 VNC/XTEST 设备(源码supports_parallel_pointer_injection与is_xvfb_process_running对此有显式探测,见 input/mod.rs)。脚本启动时也会断言DISPLAY存在、WAYLAND_DISPLAY未设置、/dev/uinput可读写。
构建驱动并运行 harness
cargo build --locked -p cua-driver --manifest-path libs/cua-driver/rust/Cargo.toml python3 libs/cua-driver/tests/linux-mpx-recovery.py \ --driver libs/cua-driver/rust/target/debug/cua-driver \ --output /tmp/mpx-recovery-evidence \ --source-sha "$(git rev-parse HEAD)"要点说明:
- 输出目录必须是不存在的新目录(harness 使用
exist_ok=False,防止污染旧证据); --source-sha必须与 harness 自身所在仓库的git rev-parse HEAD一致,否则拒绝运行(防"证据与候选版本不匹配");--driver指向刚构建的cua-driver可执行文件;- harness 只会启动它自己拥有的daemon 与 GTK fixture,信号只发给 harness 所属进程,不会触碰外部进程。
Harness 内部结构
harness(tests/linux-mpx-recovery.py)通过--fixture参数自举启动一个 GTK3 窗口:
Gtk.Entry(可访问名 "Keyboard recovery oracle")记录按键文本事件;Gtk.DrawingArea(可访问名 "MPX scroll canvas",650×400,注册SCROLL_MASK | SMOOTH_SCROLL_MASK)记录滚动事件;- 所有事件以 JSON Lines 追加写入
fixture.jsonl(event字段 + 时间戳 + 载荷)。
daemon 通过cua-driver serve --socket ... --dangerously-bypass-approvals启动(测试环境专用参数),停止通过cua-driver --socket ... stop;每次 XInput 快照用xinput list --short落盘,窗口状态用get_window_state截图取证。
覆盖的验证矩阵
| 场景 | 断言 |
|---|---|
| 正常输入与干净退出(normal-input-and-clean-exit) | 启动后创建 master 对,正常滚动投递到 fixture;干净停止后不再存在属主为自身 PID 的设备 |
| 背景按键拒绝 | 无delivery_mode的press_key返回background_unavailable时走 foreground 路径,否则验证文本到达 |
| SIGTERM 中断恢复(term-restart-recovery) | 长滚动(amount=50)进行中 SIGTERM 杀掉 daemon → 设备残留为孤儿 → 只读describe不触发回收 → 新 daemon 启动后孤儿被回收、既有未属主 master 被保留、键盘可用 |
| SIGKILL 中断恢复(kill-restart-recovery) | 同上,信号换 SIGKILL |
| 暂停的存活对端保留并恢复(paused-live-peer-preserved-and-resumed) | 对端 daemon SIGSTOP 暂停,主 daemon 停止再重启,暂停对端设备不被回收;SIGCONT 后滚动与键盘恢复正常 |
| 只读 describe 不回收 | cua-driver describe scroll后孤儿设备数量不变 |
| 最终不变量 | final时刻的 master 集合等于baseline-after-startup,proof.json记录passed: true |
所有案例逐一写入输出目录,最终产出proof.json(含source_sha、各case的passed与证据计数),供 CI 与人工审计。
测试体系与边界注意事项
- 聚焦属主单测:
mpx_owner模块内嵌了名称往返、长度上限、非法后缀/前缀、存活/元数据不可读/外来域保留、死亡与 PID 复用判定、proc-stat 畸形 comm 解析等单测(mpx_owner.rs),它们不依赖真实 X 环境,可在任何 CI 上快速运行; - 规范 Linux 桌面 harness 需单独运行:文档要求对最终候选版本另行执行规范的 Linux 桌面端到端 harness(如 scripts/ci/linux/run-rust-e2e.sh 及 gnome/kde/xorg 变体),聚焦门禁只作为原生证据的补充;
- Wayland 不受影响:回收逻辑对 Wayland 会话完全不生效,行为无任何改变;
- KDE/X11 防护:源码在 KDE Plasma X11 会话下会整体禁用 uinput 热插拔路径(
kde_x11_uinput_hotplug_is_unsafe,见 input/mod.rs),避免 Qt 客户端崩溃,恢复逻辑与之正交; - 恢复滞后性:SIGTERM/SIGKILL 遗留的设备在下一个独立 daemon 启动前不会被回收,嵌入式 SDK 直接启动也不执行回收,这是文档明确的契约而非缺陷。
总结
cua-driver 的 Linux MPX 恢复方案把"属主身份"从进程外部信息收敛为XIAddMaster调用中原子的设备名编码:以 boot_id + PID 命名空间 + 有效用户派生属主域,以 PID +starttime识别进程与 PID 复用,以 nonce 区分同进程的多次创建,再配合"X server grab + procfs 读取 + 内核 ESRCH 三重证据"的保守判定,实现了既能在守护进程重启时自动清理由 SIGTERM/SIGKILL 造成的孤儿 master 设备对、又绝不误伤存活对端、外来属主域与旧版设备的恢复契约。读者可借助 linux-mpx-recovery.py 在真实 Xorg 桌面上完整复现这一生命周期,并产出可归档的proof.json原生证据。
【免费下载链接】cuaScale computer-use 2.0 with open-source drivers, cross-OS fleets, and benchmarks for training, evaluation, and data generation.项目地址: https://gitcode.com/GitHub_Trending/cua/cua
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考