普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月12日程序员的喵

macOS 奇怪的安全扫码机制

作者 yukang
2026年2月13日 00:55

今天跑 Rust 编译器测试的时候又发现非常地慢,CPU 资源根本无法利用起来,我记得几年前碰到过这个问题,当时我写了篇文章分享出来,并发现很多人都有同样的困扰。

而我今天碰到的这个问题虽然现象一样,但解决方法又不同了。我不确定是 macOS 系统更新,亦或是我更新了 VS Code 造成的。

问题

复现脚本很简单,循环创建随机命名的 shell 脚本,然后对比首次和再次执行的耗时:

#!/bin/bash
rm -rf /tmp/speed_test
mkdir -p /tmp/speed_test

for i in {1..10}; do
    FILENAME=$(openssl rand -hex 10)
    echo $'#!/bin/sh\necho Hello' > "/tmp/speed_test/$FILENAME.sh"
    chmod a+x "/tmp/speed_test/$FILENAME.sh"
    FILE="/tmp/speed_test/$FILENAME.sh"

    first=$(TIMEFORMAT="%R"; (time $FILE > /dev/null) 2>&1)
    second=$(TIMEFORMAT="%R"; (time $FILE > /dev/null) 2>&1)
    echo "第一次: $first  第二次: $second"
done

在 VS Code 终端的输出:

第一次: 0.525  第二次: 0.007
第一次: 0.290  第二次: 0.009
第一次: 0.280  第二次: 0.007
第一次: 0.272  第二次: 0.008
第一次: 0.307  第二次: 0.008
...

差距大概 30-50 倍。换到 Warp 终端跑同一个脚本,两次都在 0.006s 左右。

定位:syspolicyd

应该不是我上篇文章提到的 SIP 问题,我确定 System Settings → Privacy & Security → Developer Tools 中已经加入了 VS Code。

看起来也不像是文件系统缓存的原因,因为 0.2-0.5 秒远超磁盘缓存的量级。用 log show 看了下系统日志:

log show --predicate 'subsystem == "com.apple.syspolicy.exec"' --last 2m --style compact

输出大量这样的记录:

GK performScan: PST: (path: 8d0e4c2de41c3e77), (team: (null)), (id: (null)), (bundle_id: (null))
Error Domain=NSOSStatusErrorDomain Code=-67062
GK evaluateScanResult: 2, PST: (path: 8d0e4c2de41c3e77), ... (bundle_id: NOT_A_BUNDLE), 0, 0, 1, 0, 7, 7, 0

从日志上看每次执行新文件 syspolicyd 都会做一次 GK performScan。这就是 macOS 的 Gatekeeper 安全扫描——对首次执行的新可执行文件做代码签名验证和恶意软件检查。扫描结果会被缓存,所以同一个文件第二次执行就快了。

进一步验证:我们把测试脚本里改成 (time /bin/sh $FILE > /dev/null) 2>&1,这样就是直接通过 sh 来执行:

直接执行 ./script.sh  → 0.248s (触发 execve → Gatekeeper 扫描)
/bin/sh ./script.sh   → 0.006s (只是让 /bin/sh 读取文件,不触发安全扫描)

原因确认了。当用 ./script.sh 执行时,内核的 execve 系统调用会触发 AppleSystemPolicy.kext 中的 MACF hook (mpo_proc_notify_exec_complete),通知 syspolicyd 进行评估。而 /bin/sh script.sh 只是让已受信的 /bin/sh 进程读取文件内容来解释执行,不触发 execve 的安全检查路径。

Full Disk Access 有效

接着试了 System Settings → Privacy & Security → Full Disk Access,给 VS Code 完全磁盘访问权限。重启 VS Code,再跑脚本:

第一次: 0.005  第二次: 0.005
第一次: 0.005  第二次: 0.005
第一次: 0.006  第二次: 0.006
...

问题消失了。syspolicyd 日志中的 performScan 也不再出现。

为什么 Full Disk Access 有效

Full Disk Access (FDA) 在 macOS 的 TCC (Transparency, Consent, and Control) 框架中对应的是 kTCCServiceSystemPolicyAllFiles 权限。这个权限的含义远超“磁盘访问“——它实际上是 TCC 框架中最高级别的信任授权。

macOS 会追踪每个进程的 responsible process(负责进程)。在 VS Code 终端中敲的命令,它的 responsible process 是 VS Code 本身。当 AppleSystemPolicy.kext 的 MACF hook 拦截到 execve 后,会检查 responsible process 的信任级别。拥有 FDA 授权的进程被识别为高信任来源,syspolicyd 会走快速路径,跳过完整的 Gatekeeper 扫描。

而 Warp 这些原生终端,因为我已经加入系统默认信任的开发工具列表,所以它们派生的子进程一开始就不会触发完整扫描。

需要说明:Apple 没有公开文档化这个具体流程。上面的描述来自实验推断和社区逆向分析,不是官方说法。

一个有趣的细节:信任会被“记住“

发现 FDA 有效之后,我尝试反向验证:把 VS Code 从 FDA 列表中移除,重启 VS Code,再跑脚本。

结果:仍然很快。问题没有复现。

syspolicyd 的扫描评估结果存储在 /var/db/SystemPolicyConfiguration/ExecPolicy 这个 SQLite 数据库中(35MB),同时 AppleSystemPolicy.kext 在内核中维护了一个运行时缓存:

$ sysctl security.mac.asp.stats.cache_entry_count
security.mac.asp.stats.cache_entry_count: 4700

也就是说,当 VS Code 拥有 FDA 时,它被评估为可信 responsible process,这个信任结果被持久化了。移除 FDA 后,历史记录并不会被清除。macOS 的安全评估系统是“学习型“的——它记住过去的信任决策。

要彻底重现原来的问题,可能需要重启 Mac 清除内核缓存,或者更极端地清理 ExecPolicy 数据库。

总结

如果你也遇到类似的问题——新编译的程序、新创建的脚本首次执行莫名其妙地慢,可以检查一下是不是 Gatekeeper 的锅:

# 查看最近的 syspolicyd 扫描记录
log show --predicate 'subsystem == "com.apple.syspolicy.exec"' --last 5m --style compact | grep performScan

解决方案按排序:

  1. 给你的程序加如到 Developer Tools 列表
  2. 给你的程序加 Full Disk Access

参考

❌
❌