Summary
终端设备查询响应(CPR / DA / DSR)泄漏进 stdin,被 rustyline 当作按键事件读入,导致输入行被注入乱码(如 0;115;0cRRR),破坏用户输入。
Use case
当 aish 转发 PTY 子进程输出(如远程 ssh/telnet 会话的 prompt tool、tmux/screen 初始化、man/less 等)到真实终端时,子进程发出的设备查询 请求(CPR ESC[6n、DA2 ESC[>c、DA1 ESC[c、DA3 ESC[=c、DSR ESC[5n)被原样转发到真实终端。真实终端响应这些查询,响应字节(ESC[2;2R、ESC[>0;115;0c、ESC[?64;1c)进入 stdin 队列。
此时 shell 处于 cooked(canonical + ECHO)模式(两次 readline 之间),rustyline 把这些响应字节当作按键事件读取,表现:
- 输入行被插入乱码片段(如
0;115;0cRRR 拼接到正在输入的命令里)
- 下一个 prompt 的输入被破坏,用户不得不手动清除
- aish 自身调用
cursor::position()(ESC[6n)的 CPR 响应也会泄漏,造成同样的污染
Proposed solution
双层防御,互为兜底:
-
输出端(aish-pty 的 write_stdout_all):剥离转发到真实终端的设备查询 请求 序列(CPR / DA1 / DA2 / DA3 / DSR 请求),从源头阻止真实终端被诱导响应。真正的 TUI / 远程程序(vim / less / ssh / tmux …,经 is_interactive_command / is_session_command 白名单判定)走 passthrough 透传,保留其查询能力。passthrough 标志用引用计数(AtomicUsize)而非 bool,使嵌套 / 并发命令正确(外层 guard 直到所有内层 guard 退出才关闭)。
-
输入端(aish-shell 的 drain_terminal_responses):每次 readline 前,以非阻塞 poll 探测 stdin;若有残留,进 raw 模式读取,丢弃前导完整 report 序列(ESC[…R / ESC[?…c / ESC[>…c / ESC[=…c / DSR ESC[…n / text-area ESC[…t),剩余字节(真实用户 type-ahead)用 TIOCSTI 重新注入以保留。当 TIOCSTI 被内核禁用(dev.tty.legacy_tiocsti=0)导致非 report 字节无法放回时,记 warn 日志(含 tail_len / reinjected 字段)便于诊断。
Alternatives considered
- 仅输出端剥离:覆盖大部分场景,但 aish 自身
cursor::position() 的 CPR 响应仍会泄漏,且无法处理查询请求跨 8 KB read 边界拆分时的漏剥离,需输入端兜底。
- 仅输入端 drain:可行,但每次 readline 都进 raw 模式探测开销较大,且无法阻止真实终端被诱导响应(终端可能闪烁 / 刷新)。
- 当前双层方案:源头治理 + 兜底清理,开销低(无 ESC 时 fast-path 零分配;无 pending 时单次
poll(fd,0) 立即返回,无 prompt 延迟)。
Additional information
strip_device_queries 精确区分 请求 与 响应:请求侧 private marker 只认 > / =(不认 ?),避开误删 DA1 响应 ESC[?64;1c;响应因参数带 ; 自然落出 digit 扫描而被保留。
- 测试:
strip_device_queries 7 个单测、report_prefix_len 8 个单测;cargo clippy --all-targets -- -D warnings 零警告。
TIOCSTI 在 Linux 6.2+ 部分发行版默认禁用;该场景下纯 report 清理不受影响(无需 reinject),仅「report + 用户输入混合」时丢失用户输入并记日志——触发条件苛刻(prompt 显示前用户基本不会在打字)。
Summary
终端设备查询响应(CPR / DA / DSR)泄漏进 stdin,被 rustyline 当作按键事件读入,导致输入行被注入乱码(如
0;115;0cRRR),破坏用户输入。Use case
当 aish 转发 PTY 子进程输出(如远程 ssh/telnet 会话的 prompt tool、tmux/screen 初始化、man/less 等)到真实终端时,子进程发出的设备查询 请求(CPR
ESC[6n、DA2ESC[>c、DA1ESC[c、DA3ESC[=c、DSRESC[5n)被原样转发到真实终端。真实终端响应这些查询,响应字节(ESC[2;2R、ESC[>0;115;0c、ESC[?64;1c)进入 stdin 队列。此时 shell 处于 cooked(canonical + ECHO)模式(两次 readline 之间),rustyline 把这些响应字节当作按键事件读取,表现:
0;115;0cRRR拼接到正在输入的命令里)cursor::position()(ESC[6n)的 CPR 响应也会泄漏,造成同样的污染Proposed solution
双层防御,互为兜底:
输出端(
aish-pty的write_stdout_all):剥离转发到真实终端的设备查询 请求 序列(CPR / DA1 / DA2 / DA3 / DSR 请求),从源头阻止真实终端被诱导响应。真正的 TUI / 远程程序(vim / less / ssh / tmux …,经is_interactive_command/is_session_command白名单判定)走 passthrough 透传,保留其查询能力。passthrough 标志用引用计数(AtomicUsize)而非 bool,使嵌套 / 并发命令正确(外层 guard 直到所有内层 guard 退出才关闭)。输入端(
aish-shell的drain_terminal_responses):每次 readline 前,以非阻塞poll探测 stdin;若有残留,进 raw 模式读取,丢弃前导完整 report 序列(ESC[…R/ESC[?…c/ESC[>…c/ESC[=…c/ DSRESC[…n/ text-areaESC[…t),剩余字节(真实用户 type-ahead)用TIOCSTI重新注入以保留。当TIOCSTI被内核禁用(dev.tty.legacy_tiocsti=0)导致非 report 字节无法放回时,记warn日志(含 tail_len / reinjected 字段)便于诊断。Alternatives considered
cursor::position()的 CPR 响应仍会泄漏,且无法处理查询请求跨 8 KB read 边界拆分时的漏剥离,需输入端兜底。poll(fd,0)立即返回,无 prompt 延迟)。Additional information
strip_device_queries精确区分 请求 与 响应:请求侧 private marker 只认>/=(不认?),避开误删 DA1 响应ESC[?64;1c;响应因参数带;自然落出 digit 扫描而被保留。strip_device_queries7 个单测、report_prefix_len8 个单测;cargo clippy --all-targets -- -D warnings零警告。TIOCSTI在 Linux 6.2+ 部分发行版默认禁用;该场景下纯 report 清理不受影响(无需 reinject),仅「report + 用户输入混合」时丢失用户输入并记日志——触发条件苛刻(prompt 显示前用户基本不会在打字)。