04 · 安全删除内核
这不是一个
rm包装器,而是一条从语法、语义、权限到恢复策略的多阶段验证管线。
总调用链
候选路径
│
├─► `validate_path_for_deletion`
│ 绝对路径 / 控制字符 / `..` / 关键根目录 / inode 别名 / symlink 祖先
│
├─► `should_protect_path` + 白名单
│ 应用身份 / 系统数据 / EDR / 用户规则
│
├─► `mole_delete(mode=trash|permanent)`
│ 大小、dry-run、日志、父目录可信度
│
└─► `safe_remove` / Trash staging / `safe_sudo_remove`
在副作用前重复结构检查与权限检查第一层:路径必须结构正确
validate_path_for_deletion 拒绝空值、相对路径、包含 .. 路径段和控制字符的输入。关键根目录不只包括 /、/System、/usr,还覆盖 /Applications、/Users 等“目录本身不能删除、子项可能允许”的边界。
单纯字符串比较不够。实现会考虑大小写不敏感文件系统、inode 指向同一对象、路径祖先中的符号链接,并对解析后的目标再次执行保护检查。这是在防止 /tmp/link/child 语法上无害、实际落到系统目录的情况。
第二层:删除模式必须显式
mole_delete 只接受 trash 或 permanent。Dry run 记录计划但不产生副作用。大小不可得时使用 unknown,避免错误的 0 B 成为误导性证据。
最关键的契约是:Trash 失败不会切换成永久删除。可恢复性是用户选择的一部分,不是“尽力而为”的实现细节。
第三层:高权限路径防 TOCTOU
调用 sudo 前,_mole_privileged_path_has_mutable_ancestor 向上检查每个祖先:
- 是否为符号链接;
- 是否由 root 所有;
- group/other 是否可写;
- 元数据能否可靠读取;
- ACL 是否允许非预期写入。
只要祖先可被普通用户替换,先验证、后 sudo 的窗口就可能被利用。Mole 对无法判断的情况拒绝执行,并且在真正删除前再次检查。
Trash 的两条路径
普通用户文件可以由 Trash CLI 或 Finder/AppleScript 移入 ~/.Trash。高权限文件更复杂:实现先在 root 拥有的 /Library/MoleTrashStaging 中进行同文件系统 rename,再调整所有权,最后交给用户侧移动。若最后一步失败,文件留在可恢复 staging,并报告位置,而不是丢失。
批量删除不是绕过检查
safe_find_delete 将深度限制为 5,并对每个候选执行保护与白名单判断。safe_sudo_find_delete 为性能可以批量交给 xargs,但批处理之前仍逐项验证,并在之后核对结果。批量化只优化系统调用,不能改变信任模型。
删除证据
删除既进入通用 operations 日志,也写入独立的 TSV 法证日志。history 能把会话与删除记录渲染为文本或 JSON。记录拒绝和失败同样重要,因为“没有删掉”可能来自保护策略,也可能来自权限或运行时错误。
关键源码索引
file_ops.sh#L106-L276:关键路径与删除验证。file_ops.sh#L380-L671:普通删除、符号链接与 sudo 祖先检查。file_ops.sh#L762-L1040:mole_delete与 Trash 路由。file_ops.sh#L1321-L1435:安全 find 删除。
下一章解释路径为何“结构安全”之后仍可能被保护:文件所属的应用和数据语义也是权限的一部分。