Zsh 退出时为什么会吞历史记录:一条信号中断错误潜伏十年
作者:林岚|OC 开发者生态编辑
作者:林岚|OC 开发者生态编辑
开发者 Michael Stapelberg 在技术复盘中披露,Zsh 一个可能截断历史文件的错误已在 5.9.2 修复。错误触发于 Shell 退出并压缩历史文件时:读取旧文件若被信号中断,后续保存逻辑没有识别“只读了一部分”,仍用不完整内容覆盖原文件。
一句话结论:这不是多个终端同时追加导致的普通竞争,而是一个读取中断状态没有跨函数传递的错误;安全重命名只能保证文件完整落盘,不能保证内容完整。
Zsh 平时可以把命令增量追加到 ~/.zsh_history。退出时,它会重新读取历史、应用条目数量和去重选项,再写入 .zsh_history.new,最后用新文件替换旧文件。这个“写临时文件再重命名”的模式通常能避免半写文件,却挡不住上游已经只读到一半。
作者先用 inotify 看到文件替换,再用 fatrace 确认执行进程,最后通过 bpftrace 比较正常和异常读写长度。决定性证据来自一个带调试符号的修改版 Zsh:当新文件行数异常偏少时主动崩溃,并保留 core dump。

core dump 显示 readhistfile 检测到 ERRFLAG_INT 后提前退出,lasthist.interrupted 也已置位;但 savehistfile 没有在替换文件前检查这个状态。作者频繁用 Ctrl+D 退出嵌套 SSH Shell、再用 Ctrl+C 中断,使这个罕见时序更容易出现。最终修复进入 Zsh 5.9.2。
这个问题据称存在约十年,说明低概率数据丢失最难靠普通测试捕获。作者还让多种前沿模型根据症状和 bpftrace 记录找错,部分模型能定位中断链路,但稳定结论仍依赖高质量运行证据。AI 可以加速读源码,不能替代复现器。
关键事实
- 影响:退出重写历史时,可能用不完整内容覆盖原文件
- 根因:读取被信号中断,保存逻辑未检查中断状态
- 调试:inotify、fatrace、bpftrace、主动崩溃与 core dump
- 修复:已进入 Zsh 5.9.2,发布于 2026 年 7 月 12 日
OC 判断
问题不在原子重命名,而在原子操作之前的数据是否可信。对任何“读旧状态、生成新状态、整体替换”的程序,都应该把中断、短读和取消当作失败传播,不能把部分结果包装成成功。
为什么重要
- 对用户:遇到历史异常截断应升级至 Zsh 5.9.2,并保留备份。
- 对开发者:临时文件加重命名只解决写入原子性,不解决输入完整性。
- 对调试者:为低概率故障设计触发式崩溃,常比长期收集海量日志更有效。
评论
围绕这篇文章补充信息、提出问题或分享观察。