Skip to content

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 只接受 trashpermanent。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。记录拒绝和失败同样重要,因为“没有删掉”可能来自保护策略,也可能来自权限或运行时错误。

关键源码索引

下一章解释路径为何“结构安全”之后仍可能被保护:文件所属的应用和数据语义也是权限的一部分。

独立源码研究笔记 · 非 Mole 官方文档