$ 跳轉到主要內容
概覽
0% · 剩餘 ...
0%
檔名的語義陷阱:從等價關係看 macOS 的安全設計
$ cat sys/security/macos_filename_semantics.md

# 檔名的語義陷阱:從等價關係看 macOS 的安全設計

作者:
日期: 2025年12月8日 13:56
閱讀時間: 預計 5 分鐘
sys/security/macos_filename_semantics.md

本頁面系 AI 翻譯,如有疏漏請以原博文為準。

開篇

什麼是「相同」?

這個看似簡單的問題,在電腦系統中卻充滿陷阱。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

許可協議

除特別聲明以外,本部落格所有文章與素材內容均採用 創用CC 姓名標示 - 非商業性 - 相同方式分享 4.0 國際授權條款 (CC BY-NC-SA 4.0)

✓ 已複製!