cua-driver Linux X11 MPX 孤儿设备回收:基于版本化属主的 master 指针/键盘对自动恢复机制
2026/9/14 5:29:06 网站建设 项目流程

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)",包含五个要素:

  1. 属主域(domain):由内核启动标识(/proc/sys/kernel/random/boot_id)、PID 命名空间(/proc/self/ns/pid)与有效用户(geteuid)经 SHA-256 派生出的 16 字节哈希;
  2. PID:属主进程 ID(4 字节);
  3. 进程启动时刻(start time):取自/proc/{pid}/statstarttime字段(8 字节),用于识别 PID 复用;
  4. 唯一 nonce:每次创建递增的 8 字节计数器;
  5. 设备角色后缀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()构造当前进程属主时做了两道命名空间自检:

  1. 解析/proc/self/stat得到首字段 PID,若与std::process::id()不一致,说明 procfs 暴露的是另一个 PID 命名空间,直接报错;
  2. 再读取/proc/{self_pid}/statstarttime与自身比对,不一致同样报错("procfs does not identify this PID namespace")。

随后以boot_id、PID 命名空间 inode 与geteuid拼接后 SHA-256,取前 16 字节作为domain。这三个输入共同界定了"同一内核启动 + 同一 PID 命名空间 + 同一有效用户"的属主域,使跨主机/跨容器共享 X server 时互不误伤。

starttime的解析(start_ticks)还处理了一个经典陷阱:/proc/{pid}/statcomm字段可能包含空格与右括号,必须用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
  • 拒绝任何额外后缀:单测验证了keyboardXTEST pointerpointer extraCUA legacy mp-123-1 pointerCUA v1.bad.bad.bad pointerordinary pointer全部返回None(mpx_owner.rs);
  • 名称长度受 uinput 约束(UINPUT_MAX_NAME_SIZE80 字节、evdev 断言留 1 字节,实际可用 78 字节),单测确认即使 PID 取i32::MAXstart_ticksu64::MAX,完整名称仍不超限。

过期判定(stale_in,mpx_owner.rs)

判定顺序设计为"失败保守"(fail-closed,宁可保留):

  1. domain不一致 → 非过期(外来属主域,直接保留);
  2. /proc/{pid}/stat可读且starttime与设备名记录的不同 →过期(PID 已被复用);
  3. 读取返回NotFound→ 要求内核独立报告kill(pid, 0)返回ESRCH才算过期。之所以额外要求ESRCH,是因为hidepid或权限不足会让/proc条目"看起来不存在",只有内核明确说"没有这个进程"才可信;
  4. 其他错误(PermissionDeniedInvalidDataInterrupted)→ 一律视为未过期

单测矩阵印证了这一契约: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的执行序列:

  1. 存在WAYLAND_DISPLAY→ 直接返回;
  2. Owner::current()失败 → 直接返回(不冒险);
  3. XGrabServer抢占服务器(grab 期间只做本地 procfs 读取与kill(pid, 0)探测,不涉及网络或任意文件系统读取);
  4. XIQueryDevice枚举全部设备,仅处理XIMasterPointer类型;
  5. 逐个用Owner::from_pointer_name解析名称,stale_in(&owner)为真则调用remove_master_pointerXIRemoveMasterreturn_mode = XIAttachToMaster归还到 Virtual core pointer/keyboard,见 input/mod.rs),并记录 tracing 日志;
  6. 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_injectionis_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.jsonlevent字段 + 时间戳 + 载荷)。

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_modepress_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-startupproof.json记录passed: true

所有案例逐一写入输出目录,最终产出proof.json(含source_sha、各casepassed与证据计数),供 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询