$ 跳转到主要内容
概览
0% · 剩余 ...
0%
文件名的语义陷阱:从等价关系看 macOS 的安全设计
$ cat sys/security/macos_filename_semantics.md

# 文件名的语义陷阱:从等价关系看 macOS 的安全设计

作者:
日期: 2025年12月8日 13:56
阅读时间: 预计 5 分钟
sys/security/macos_filename_semantics.md

开篇

什么是「相同」?

这个看似简单的问题,在计算机系统中却充满陷阱。2025 年 4 月,Linus Torvalds 在 Linux 内核邮件列表上针对 bcachefs 的 case-folding 功能发表了一封措辞激烈的邮件1。他的核心论点可以归结为一句话:文件名应该只是一串字节,文件系统不应该试图「理解」它们。

这不仅仅是一个技术偏好的问题。当我们开始赋予文件名语义,如让系统理解 A 和 a 是相同的,或者 é 和 e+´ 是相同的,我们就打开了一个潘多拉的盒子。

让我们进一步理解这个问题的本质。

文件名的本质:标识符还是名字?

在图书馆中,每本书有两种标识方式:书名和索书号。书名是有语义的,它告诉你这本书是关于什么的;索书号是不透明的,它只是一个唯一的标识符,用于在书架上定位这本书。

Unix 的设计者选择了「索书号」模型。

在 Unix 的设计哲学中,文件名是一个不透明的字节序列。内核的职责很简单:将这个字节序列映射到一个 inode(索引节点)。正如 O’Reilly 出版的「Understanding the Linux Kernel」所述:「A Unix file is an information container structured as a sequence of bytes; the kernel does not interpret the contents of a file.」2 这种「不解释」的哲学同样适用于文件名。

这个设计选择的深层原因是什么?

简单性带来可预测性。当文件名只是字节序列时,系统的行为是完全确定的:两个文件名相同当且仅当它们的字节序列完全相同。没有歧义,没有特殊情况,没有文化依赖。VFS(Virtual File System)层通过 dentry cache 将路径名解析为 inode,但这个过程是纯粹的字节匹配3

但 macOS 选择了不同的路径。

等价关系:当相同变得复杂

在数学中,等价关系是一个满足三个性质的关系:自反性(a ~ a)、对称性(a ~ b 则 b ~ a)、传递性(a ~ b 且 b ~ c 则 a ~ c)。当我们说大小写不敏感时,我们实际上是在定义一个等价关系:A ~ a,B ~ b,以此类推。

这看起来很简单,不是吗?

但问题在于:谁来定义这个等价关系?不同的系统可能有不同的定义。

在英语中,i 的大写是 I,这似乎是显而易见的。但土耳其语有四个不同的 i:

形式有点无点
大写İ (U+0130)I (U+0049)
小写i (U+0069)ı (U+0131)

在土耳其语中,i 的大写是 İ(带点的大写 I),而 I 的小写是 ı(无点的小写 i)4。这意味着,如果一个安全检查使用英语规则将 FILE 转换为小写得到 file,而文件系统使用土耳其语规则得到 fıle,这两个字符串就不会匹配——即使它们「应该」是同一个文件名。

WARNING

这不是理论上的问题。Jeff Atwood 在 Coding Horror 博客中记录了一个真实案例:当应用程序在土耳其语 locale 下运行时,字符串 INTEGER 转换为小写会得到 ınteger 而不是 integer,导致程序逻辑完全失效5

这揭示了一个深刻的问题:大小写转换不是一个普遍的、与文化无关的操作。当我们在文件系统层面实现大小写不敏感时,我们必须选择一个特定的规则——而这个选择可能与用户空间程序使用的规则不一致。

Unicode 规范化:更深的兔子洞

如果大小写不敏感已经够复杂了,Unicode 规范化则把问题推向了另一个维度。

让我们从一个简单的问题开始:é 是一个字符还是两个字符?

在 Unicode 中,答案是「都可以」。é 可以用单个码点 U+00E9(LATIN SMALL LETTER E WITH ACUTE)表示,也可以用两个码点 U+0065(LATIN SMALL LETTER E)+ U+0301(COMBINING ACUTE ACCENT)表示。这两种表示在视觉上完全相同,但在字节层面却截然不同:

预组合形式(NFC):C3 A9 -> é
分解形式(NFD): 65 CC 81 -> e + ́ → é

Unicode 标准定义了四种规范化形式:NFC、NFD、NFKC、NFKD6。NFC 倾向于使用预组合字符,NFD 倾向于将字符分解为基字符加组合标记。

HFS+ 选择了 NFD。根据 Apple 的技术文档,HFS+ 使用一种「非常接近 Unicode Normalization Form D」的规范化形式7。这意味着当你创建一个名为 café 的文件时,系统会自动将 é 分解为 e + ´ 两个码点。

这个设计决策有其合理性:通过强制规范化,HFS+ 确保了同一个「字符」只有一种表示方式,从而避免了用户创建两个「看起来相同但实际不同」的文件。

但这里有一个关键的问题:HFS+ 的规范化规则基于 Unicode 3.2,而这个规则已经无法随着 Unicode 标准的演进而更新,因为「这种演进会使现有的 HFS+ 卷失效」8

IMPORTANT

这是一个停留在 1998 年的规范化实现,却要服务于 2025 年的用户。Unicode 标准已经从 3.2 演进到了 17.0(于 2025 年 9 月 9 日发布),新增了数万个字符,但 HFS+ 的规范化规则永远停留在了过去。

2017 年,Apple 推出了 APFS 来取代 HFS+。APFS 做出了一个重要的改变:它不再强制进行 Unicode 规范化,而是 normalization-preserving but normalization-insensitive9。这意味着 APFS 会保留你输入的原始字节序列,但在比较文件名时仍然会考虑规范化等价性。

这个改变解决了一些问题,但也带来了新的问题。从 HFS+ 迁移到 APFS 时,原本被规范化的文件名会保持其 NFD 形式,而新创建的文件可能使用 NFC 形式。在某些边缘情况下,这可能导致「看起来相同但实际不同」的文件名共存于同一目录中。

安全漏洞的本质:等价关系的不一致

现在我们可以理解安全漏洞的本质了。

当安全检查程序和文件系统使用不同的等价关系时,就会产生一个「缝隙」。攻击者可以构造一个文件名,它在安全检查程序看来是安全的,但在文件系统看来却等价于一个危险的文件名。

Torvalds 在邮件中精准地描述了这个问题:

Security issues like “user space checked that the filename didn’t match some security-sensitive pattern”. And then the shit-for-brains filesystem ends up matching that pattern anyway

让我们看一个具体的例子。

2021 年 3 月,Git 项目披露了一个严重漏洞 CVE-2021-2130010。这个漏洞特别影响使用大小写不敏感文件系统的 Windows 和 macOS 用户。

漏洞的原理涉及 Git 的 lstat 缓存机制。当 Git 检出文件时,它会维护一个缓存来减少系统调用。攻击者可以构造一个恶意仓库,其中包含两个文件:A 和 a。在大小写敏感的文件系统上,这是两个不同的文件;但在大小写不敏感的文件系统上,它们会发生冲突。

攻击的关键在于:Git 的内部逻辑(基于大小写敏感的假设)和文件系统的行为(大小写不敏感)之间存在不一致。攻击者利用这个不一致,可以让 Git 在检出过程中执行任意代码。

Unicode 规范化带来的安全问题更加微妙。根据 Black Hat USA 2019 的研究论文「Host/Split: Exploitable Antipatterns in Unicode Normalization」,当安全决策基于 Unicode 字符串进行,而后续处理使用不同的规范化形式时,就会产生可利用的漏洞11

考虑这样一个场景:安全软件检查文件名是否匹配 /etc/passwd 这个敏感路径。

攻击者创建一个文件,名称中包含不可见字符或 Unicode 变体。安全软件检查字符串,发现它不等于 /etc/passwd,于是放行。

然而,文件系统在底层处理时,可能会将这些 Unicode 变体规范化为与 /etc/passwd 等价的形式,从而绕过安全检查。

CERT/CC 在 VU#999008 中记录了类似的问题:编译器允许 Unicode 控制字符和同形字符(homoglyph)出现在源代码中,这可能被用于在代码审查时隐藏恶意代码12

TOCTOU:时间维度上的等价关系问题

还有一类更加微妙的漏洞:TOCTOU(Time-of-Check to Time-of-Use)。

TOCTOU 漏洞的本质是:在检查和使用之间存在一个时间窗口,攻击者可以在这个窗口内改变系统状态,使检查结果失效13

在文件系统的上下文中,这个问题与文件名的语义解释密切相关。让我们思考一下文件访问的过程:

  1. 程序使用文件名请求访问一个文件
  2. 内核将文件名解析为 inode
  3. 内核检查权限
  4. 内核返回文件描述符

问题在于:步骤 1 和步骤 2 之间,文件名到 inode 的映射可能发生变化。攻击者可以在这个窗口内,将文件名重新指向另一个文件。

NOTE

这里有一个关键的技术细节:虽然文件名到 inode 的映射是易变的,但 inode 到文件描述符的映射是稳定的14。一旦你获得了一个文件描述符,它就直接指向 inode,不再依赖文件名。这就是为什么 CERT SEI 建议「只打开关键文件一次,然后通过文件描述符而不是文件名来执行所有需要的操作」。

macOS 的大小写不敏感和 Unicode 规范化使 TOCTOU 问题更加复杂。当安全检查使用一种文件名表示形式,而实际文件操作使用另一种等价但不同的表示形式时,TOCTOU 窗口就会扩大。

USENIX FAST’23 的论文「Unsafe at Any Copy: Name Collisions from Mixing Case Sensitivity」系统性地研究了这个问题15。研究发现,不同文件系统的大小写折叠规则和规范化技术存在差异。例如,temp_200K(其中 K 是 Kelvin Sign,U+212A)和 temp_200k 在 NTFS 和 APFS 上被视为相同,但在 ZFS 上被视为不同。

这种不一致性是安全漏洞的温床。

纵深防御:当文件名不可信时

面对文件名不可信的现实,Apple 选择了纵深防御(Defense in Depth)策略。

这个策略的核心思想是:既然我们无法让文件名变得可信,那就不要依赖文件名来建立信任。相反,我们在多个层面建立独立的安全屏障,每个屏障都使用不同的信任基础。

让我们看看 Apple 是如何实现这个策略的。

Merkle 树与签名系统卷

macOS Big Sur(11.0)引入了签名系统卷(Signed System Volume,SSV)机制16。SSV 的核心思想是:用加密哈希来验证系统完整性,而不是依赖文件名。

SSV 的技术实现基于 Merkle 树。Merkle 树是一种优雅的数据结构,它允许我们用一个固定大小的「根哈希」来验证任意大小的数据集的完整性17

Merkle 树的工作原理如下:

  1. 将数据分成若干块,计算每个块的哈希值(叶节点)
  2. 将相邻的哈希值配对,计算它们的组合哈希(内部节点)
  3. 重复步骤 2,直到只剩下一个哈希值(根节点)

这种结构有一个关键特性:任何数据块的修改都会导致从该叶节点到根节点的整条路径上的所有哈希值发生变化。因此,只要根哈希是可信的,我们就可以验证整个数据集的完整性。

TIP

Merkle 树的验证效率是对数级的。

要验证一个特定数据块的完整性,只需要检查从该叶节点到根节点路径上的哈希值——对于 n 个数据块,这只需要 O(logn)O(log n) 次哈希计算。这使得 SSV 可以在启动时快速验证系统完整性,而不会显著增加启动时间。

在 SSV 的实现中,系统卷上的每个文件都有一个 SHA-256 哈希值存储在文件系统元数据中。根节点的哈希值被称为 seal(封印),它涵盖了 SSV 上的每一个字节。

这个 seal 在每次 Mac 启动时由引导加载程序验证。如果验证失败,启动会被中止,用户会被提示重新安装操作系统18

这意味着什么?无论攻击者使用什么大小写混淆、Unicode 变体,甚至即使获得了 Root 权限,只要试图修改系统卷的任何内容,哈希值就会对不上,系统便拒绝启动。文件名的语义解释问题在这里变得无关紧要——因为整个卷的完整性是通过加密哈希来保证的,而不是通过文件名来保证的。

元数据标记与 SIP

在 SSV 之前,macOS 已经有了 SIP(System Integrity Protection),也被称为 rootless19。SIP 的核心思想是:用元数据标记来保护关键文件,而不是依赖文件名。

SIP 引入了一个新的 restricted 文件标志。标记为 restricted 的文件无法被修改,即使以 root 用户身份运行20

关键在于:SIP 的检查是基于 inode 的元数据标记,而不是基于文件名。当任意进程试图写入受保护的目录时,内核会检查该 inode 是否被标记为「受 SIP 保护」。即使攻击者通过 Unicode 变体或大小写混淆骗过了上层检查,当请求抵达内核时,内核会查看 inode 的 restricted 标志,然后拒绝操作。

这是一个重要的设计原则:将信任基础从文件名转移到 inode 的元数据。文件名可以被混淆,但 inode 的元数据是由内核直接管理的,不受文件名语义解释的影响。

文件描述符:绕过文件名

Apple 在开发者文档中推崇使用 NSURL(对象)和 File Descriptor(文件描述符),而不是直接使用文件路径字符串21

这个设计选择背后有深刻的安全考量。

在 Sandbox 机制下,当用户授权 App 访问某个文件时,系统会给予 App 一个 Token 而不是路径。App 可以通过这个 Token 向内核请求文件访问,内核则通过 inode 进行文件定位。

这种设计规避了复杂的路径解析、大小写匹配和 Unicode 规范化问题。更重要的是,它从根本上消除了 TOCTOU 漏洞的可能性——因为文件描述符直接指向 inode,而不是通过文件名间接引用。

TCC:基于进程身份的访问控制

TCC(Transparency, Consent, and Control)是 macOS 用于管理应用程序访问敏感数据的框架22。TCC 的核心是一个 SQLite 数据库,存储在 /Library/Application Support/com.apple.TCC/TCC.db。

TCC 的关键安全特性在于:它根据进程身份(而非文件名)来拦截访问。当攻击者试图读取用户隐私目录时,TCC 会检查请求进程的身份和权限,而不是简单地检查文件路径字符串。

TCC 数据库本身受到 SIP 保护,无法直接修改23。要干预这些数据库,攻击者必须禁用 SIP 或获得对受信任系统进程的访问权限。

设计哲学的反思

回到 Linus Torvalds 的批评,我们可以看到这不仅是技术问题,更是设计哲学问题。

Unix 的设计哲学强调简单性和正交性。文件名是字节序列,内核不解释它们的含义。这种设计的优点是可预测性和安全性——没有隐藏的语义转换,没有意外的等价关系。

macOS 选择了不同的路径,试图为用户提供更友好的体验。大小写不敏感让用户不必担心 Document.txt 和 document.txt 的区别。Unicode 规范化让用户不必理解 NFD 和 NFC 的差异。

但这种友好是有代价的。当文件系统开始理解文件名时,它就承担了定义「相同」的责任。而相同的定义是复杂的、文化相关的、不断演化的。

更深层的问题是:当我们在系统的不同层面使用不同的相同定义时,就会产生安全漏洞。安全检查程序可能使用一种等价关系,文件系统使用另一种等价关系,而攻击者正是利用这种不一致来绕过安全检查。

Apple 的纵深防御策略是一种务实的妥协。既然无法改变历史决定(大小写不敏感已经是 macOS 的默认行为),那就在更高和更低的层面建立独立的安全屏障:

  • SSV 通过加密哈希保护系统完整性
  • SIP 通过元数据标记保护关键文件
  • File Descriptor 绕过文件名,直接使用 inode
  • TCC 通过进程身份验证保护用户数据

这些机制的共同特点是:它们不信任文件名。它们使用加密哈希、元数据标记、进程身份和 inode 来建立信任,而不是依赖于可能被混淆的字符串。

结语

Torvalds 的批评提醒我们一个基本原则:安全系统不应该依赖于复杂的语义解释。

一个简单的模型,即使不完美,也往往比一个复杂的模型更可靠、更可预测。Unix 的「文件名是字节序列」正是这样一个简单模型:它放弃了「理解」文件名的能力,但换来了可预测性和安全性。

macOS 的历史证明了复杂性的代价。从 HFS+ 的 Unicode 规范化到 APFS 的规范化不敏感,从 Git 漏洞到 TOCTOU 攻击,文件名的语义解释一直是安全问题的温床。

但 Apple 的应对策略也展示了工程智慧:当我们无法消除复杂性时,我们可以通过纵深防御来限制其影响。SSV、SIP、File Descriptor、TCC——这些机制都不依赖于文件名的语义解释,它们在更底层建立了独立的信任基础。

对于开发者而言,这个故事的教训是明确的:

  • 永远不要假设文件名是唯一的或不可变的
  • 使用文件描述符而不是路径字符串进行文件操作
  • 在进行安全检查时,考虑大小写和 Unicode 规范化的影响
  • 在跨平台开发时,测试大小写敏感和不敏感两种环境

正如 Torvalds 所说,文件名应该只是一串字节。当我们开始赋予它们魔法般的含义时,我们就打开了潘多拉的盒子。


参考文献

Footnotes

  1. Phoronix. “Linus Torvalds Expresses His Hatred For Case-Insensitive File-Systems.” 2025. https://www.phoronix.com/news/Linus-Torvalds-Anti-Case-Fold

  2. Bovet, D. P., & Cesati, M. “Understanding the Linux Kernel, Second Edition.” O’Reilly Media, 2002.

  3. Linux Kernel Documentation. “Overview of the Linux Virtual File System.” https://docs.kernel.org/filesystems/vfs.html

  4. I18n Guy. “Internationalization for Turkish: Dotted and Dotless Letter I.” http://www.i18nguy.com/unicode/turkish-i18n.html

  5. Atwood, J. “What’s Wrong With Turkey?” Coding Horror, 2008. https://blog.codinghorror.com/whats-wrong-with-turkey/

  6. Unicode Consortium. “UAX #15: Unicode Normalization Forms.” https://unicode.org/reports/tr15/

  7. Apple Developer. “Technical Q&A QA1235: Converting to Precomposed Unicode.” https://developer.apple.com/library/archive/qa/qa1235/_index.html

  8. Wikipedia. “HFS Plus.” https://en.wikipedia.org/wiki/HFS_Plus

  9. Eclectic Light. “Explainer: Unicode, normalization and APFS.” 2021. https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/

  10. InfoQ. “Analyzing Git Clone Vulnerability.” 2021. https://www.infoq.com/news/2021/03/git-clone-vulnerability/

  11. Birch, J. “Host/Split: Exploitable Antipatterns in Unicode Normalization.” Black Hat USA 2019. https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization-wp.pdf

  12. CERT/CC. “VU#999008 - Compilers permit Unicode control and homoglyph characters.” 2021. https://www.kb.cert.org/vuls/id/999008

  13. MITRE. “CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition.” https://cwe.mitre.org/data/definitions/367.html

  14. CERT SEI. “FIO45-C. Avoid TOCTOU race conditions while accessing files.” https://wiki.sei.cmu.edu/confluence/display/c/FIO45-C.+Avoid+TOCTOU+race+conditions+while+accessing+files

  15. Basu, A., et al. “Unsafe at Any Copy: Name Collisions from Mixing Case Sensitivity.” USENIX FAST’23. https://www.usenix.org/system/files/fast23-basu.pdf

  16. Apple Support. “Signed system volume security.” Apple Platform Security Guide. https://support.apple.com/guide/security/signed-system-volume-security-secd698747c9/web

  17. Wikipedia. “Merkle tree.” https://en.wikipedia.org/wiki/Merkle_tree

  18. Jamf Blog. “What’s New in macOS Big Sur Security.” 2020. https://www.jamf.com/blog/whats-new-in-macos-big-sur-security/

  19. Wikipedia. “System Integrity Protection.” https://en.wikipedia.org/wiki/System_Integrity_Protection

  20. Apple Support. “System Integrity Protection.” Apple Platform Security Guide. https://support.apple.com/guide/security/system-integrity-protection-secb7ea06b49/web

  21. Apple Support. “Controlling app access to files in macOS.” Apple Platform Security Guide. https://support.apple.com/guide/security/controlling-app-access-to-files-secddd1d86a6/web

  22. Rainforest QA. “A deep dive into macOS TCC.db.” 2021. https://www.rainforestqa.com/blog/macos-tcc-db-deep-dive

  23. Huntress. “Full Transparency: Controlling Apple’s TCC.” 2024. https://www.huntress.com/blog/full-transparency-controlling-apples-tcc

~
~
sys/security/macos_filename_semantics.md
$ license --info

许可协议

除特别声明以外,本博客所有文章与素材内容均采用 知识共享署名 - 非商业性使用 - 相同方式共享 4.0 国际许可协议 (CC BY-NC-SA 4.0)

✓ 已复制!