为什么某些 Mac 应用启动缓慢:后续分析与追踪

2025年5月1日

去年我写了一篇博客文章 Mac app launches slowed by malware scan,其中提到:

我发现启动缓慢是由 syspolicyd 进程引起的,特别是 DispatchQueue "com.apple.security.syspolicy.yara"。回溯显示 syspolicyd 调用了 yr_rules_scan_file 函数。

然而,最近博主 Howard Oakley 发表了一系列博文,从 Why some apps launch very slowly 开始,到 Why some apps sometimes launch extremely slowly 结束,似乎否认了我的发现。Oakley 说:

使用任何已知的 Yara 规则进行恶意软件扫描的可能性极小,因为:

  • XProtect Yara 规则通常包括文件大小限制,导致很少有规则应用于较大的文件,从而更快地完成扫描。
  • 使用 Yara 规则进行的已知检查都会详细记录在日志条目中,并且明确说明了这些规则的来源。
  • Yara 扫描通常会报告其结果。
  • 扫描结果简洁明了,不太可能在“缓存未命中”中丢失。

我对这种否认感到非常困惑,因为我提到的回溯直接来自 spindumps (/usr/sbin/spindump),它频繁(默认情况下为 10 毫秒)地对系统上所有正在运行的进程进行采样。Spindumps 不会说谎!

Spindumps 还表明,syspolicyd 恶意软件检查是由 dlopen 函数触发的,该函数用于加载动态库。这些是 Oakley 提到的框架检查;框架本质上是捆绑的动态库。你可以在启动的应用的样本中看到一系列函数调用:

dyld4::APIs::dlopen_from(char const*, int, void*) AppleSystemPolicy::fileCheckLibraryValidation(proc*, fileglob*, long long, long long, unsigned long) AppleSystemPolicy::perform_malware_scan_if_necessary(ASPProcessInfo*, ASPEvaluationInfo*, int, ScanMeta*, int*, unsigned int, int, long long*) AppleSystemPolicy::waitForEvaluation(syspolicyd_evaluation*, int, ASPEvaluationInfo*, vnode**, evaluation_results*, long long*, char const*)

没有什么比 perform_malware_scan_if_necessary 更明显的了!而且该应用正在等待 syspolicyd 的评估,因此启动缓慢。

我试图在 Oakley 的一篇博客文章的评论中向他解释这一点,但由于某种奇怪的原因,他拒绝接受我的观点。事实上,他拒绝获取或阅读 spindump。起初,他声称这对于除像我这样的“专家”之外的任何人都太难了,但我只是从 Activity Monitor 的操作菜单中选择了“Spindump”,然后对生成的文件进行了文本搜索。(另一方面,你可以使用 spindump 命令行工具更精确地控制 spindump 的时机,甚至指定它应该等待特定应用程序启动。)

Oakley 的立场似乎是日志消息告诉了他需要知道的一切,就好像一种现象只有在被记录时才存在一样。作为一名计算机程序员,Oakley 应该更清楚,因为日志消息只有在程序员专门、有意识地在程序中编写日志记录语句时才会出现。并非所有发生在你的 Mac 上的事情都会神奇地被记录下来。

这是 Oakley 关于启动缓慢的理论:

解释这些长时间检查的最可能活动是计算应用程序的 Frameworks 文件夹中每个项目的 SHA-256 哈希值。因此,这些偶尔出现的极长启动时间很可能是由于在计算应用程序包中 Frameworks 文件夹中受保护代码的哈希值时花费的时间来“评估信任”,而这些哈希值已从其缓存中清除。

Oakley 似乎在没有经验证据的情况下声称,Mac 拥有已启动的所有应用程序的所有捆绑文件的 SHA-256 哈希缓存。但是这个缓存到底在哪里??? 我从来没有见过或听说过这样的事情。据我所知,这纯粹是 Oakley 基于一条简洁的日志消息的推测:

com.apple.syspolicy.exec Recording cache miss for <private>

具有讽刺意味的是,他的日志消息中对 com.apple.syspolicy.execAppleSystemPolicy 的引用似乎与我的 spindumps 的结果非常吻合。我不否认 某些东西 被缓存了,但我认为证据表明,缓存的是恶意软件扫描的结果,从实际角度来看,这比一堆哈希值更有意义。毕竟,恶意软件定义会定期更新以发现新发现的恶意软件,这意味着先前恶意软件扫描的结果最终会过时。然而,Oakley 的理论对我来说没有任何意义。可执行文件的代码签名在运行时加载可执行文件时 始终 检查,因此如果自上次启动以来应用程序已被修改,则无论如何都会检测到。此外,应用程序具有一些防止修改的保护措施:App Store 应用程序由 root 用户拥有,并且 App Management 功能可以防止(或者 应该防止)经过公证的应用程序被未经授权的修改。因此,我什至不理解缓存捆绑文件的 SHA-256 哈希值并定期重新计算它们的实用性。

还值得注意的是,Oakley 对 哈希性能的讨论 忽略了一个关键细节:他检查的应用程序是 universal binaries,同时具有 Intel 和 ARM 架构。这使捆绑的动态库的大小增加了一倍。Oakley 仅根据文件的大小运行了一些哈希测试,但正如 Apple 在 Oakley 的文章链接到的 技术说明 中解释的那样,每种架构都有其自己单独的代码签名:

上面的命令包括 --arch x86_64 选项以显示 Intel 代码签名。如果没有该选项,codesign 会显示运行该命令的架构的代码签名。因此,如果您使用的是 Apple silicon,您将看到 Apple silicon 代码签名。

当应用程序启动时,系统不太可能低效地花费时间检查未使用架构的代码签名,因此无论执行什么检查,都应仅根据可执行文件大小的一半进行测量。

总的来说,据我所知,这里没有什么新鲜的东西,Oakley 只是观察到了我去年已经观察到的相同现象。

Jeff Johnson