Введение
Что такое «одинаково»?
Этот, казалось бы, простой вопрос в компьютерных системах полон ловушек. В апреле 2025 года Линус Торвальдс опубликовал в списке рассылки ядра Linux резкое письмо1, касающееся функции case-folding (преобразования регистра) в bcachefs. Его основной аргумент можно свести к одной фразе: имена файлов должны быть просто последовательностью байтов, и файловая система не должна пытаться их «понимать».
Это не просто вопрос технических предпочтений. Когда мы начинаем наделять имена файлов семантикой — например, заставляя систему считать, что «A» и «a» идентичны, или что «é» (U+00E9) и «e+´» (U+0065 U+0301) — это одно и то же, мы открываем ящик Пандоры.
Давайте глубже разберемся в сути этой проблемы.
Суть имени файла: идентификатор или имя?
В библиотеке у каждой книги есть два способа идентификации: название и шифр хранения (call number). Название несет семантику, оно говорит вам, о чем эта книга; шифр хранения непрозрачен, это просто уникальный идентификатор, используемый для поиска книги на полке.
Создатели Unix выбрали модель «шифра хранения».
В философии дизайна Unix имя файла — это непрозрачная последовательность байтов. Задача ядра проста: сопоставить эту последовательность байтов с inode (индексным дескриптором). Как сказано в книге «Understanding the Linux Kernel» издательства O’Reilly: «Unix-файл — это контейнер информации, структурированный как последовательность байтов; ядро не интерпретирует содержимое файла»2. Эта философия «неинтерпретации» в равной степени относится и к именам файлов.
В чем причина такого выбора дизайна?
Простота обеспечивает предсказуемость. Когда имена файлов — это просто последовательности байтов, поведение системы полностью детерминировано: два имени файла идентичны тогда и только тогда, когда их последовательности байтов полностью совпадают. Никакой двусмысленности, никаких особых случаев, никакой зависимости от культуры. Слой VFS (Virtual File System) разрешает пути в inode через кэш dentry, но этот процесс представляет собой чистое сопоставление байтов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
Это не теоретическая проблема. Джефф Этвуд в блоге Coding Horror зафиксировал реальный случай: когда приложение запускалось в турецкой локали, преобразование строки «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 (выпущенной 9 сентября 2025 года), добавив десятки тысяч новых символов, но правила нормализации HFS+ навсегда остались в прошлом.
В 2017 году Apple представила APFS на замену HFS+. APFS внесла важное изменение: она больше не принуждает к нормализации Unicode, а является «сохраняющей нормализацию, но нечувствительной к ней» (normalization-preserving but normalization-insensitive)9. Это означает, что APFS сохранит исходную последовательность байтов, которую вы ввели, но при сравнении имен файлов все равно будет учитывать эквивалентность нормализации.
Это изменение решило одни проблемы, но породило новые. При миграции с HFS+ на APFS имена файлов, которые были нормализованы, сохранят свою форму NFD, в то время как новые файлы могут использовать форму NFC. В некоторых пограничных случаях это может привести к тому, что в одном каталоге будут сосуществовать имена файлов, которые «выглядят одинаково, но на самом деле разные».
Суть уязвимостей безопасности: несогласованность отношений эквивалентности
Теперь мы можем понять природу уязвимостей безопасности.
Когда программа проверки безопасности и файловая система используют разные отношения эквивалентности, возникает «брешь». Злоумышленник может сконструировать имя файла, которое кажется безопасным программе проверки, но эквивалентно опасному имени файла с точки зрения файловой системы.
Торвальдс в своем письме точно описал эту проблему:
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 года в проекте Git была обнаружена серьезная уязвимость CVE-2021-2130010. Она особенно затронула пользователей Windows и macOS, использующих файловые системы, нечувствительные к регистру.
Принцип уязвимости связан с механизмом кэширования lstat в Git. Когда Git извлекает (checkout) файлы, он поддерживает кэш для уменьшения количества системных вызовов. Злоумышленник мог создать вредоносный репозиторий, содержащий два файла: 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 и омографам (homoglyphs) присутствовать в исходном коде, что может быть использовано для сокрытия вредоносного кода при аудите12.
TOCTOU: проблема отношений эквивалентности во временном измерении
Существует еще один класс более тонких уязвимостей: TOCTOU (Time-of-Check to Time-of-Use).
Суть уязвимости TOCTOU заключается в наличии временного окна между проверкой и использованием, в течение которого злоумышленник может изменить состояние системы, сделав результат проверки недействительным13.
В контексте файловых систем эта проблема тесно связана с семантической интерпретацией имен файлов. Давайте подумаем о процессе доступа к файлу:
- Программа запрашивает доступ к файлу, используя имя.
- Ядро разрешает имя файла в inode.
- Ядро проверяет права доступа.
- Ядро возвращает дескриптор файла.
Проблема в том, что между шагом 1 и шагом 2 сопоставление имени файла и inode может измениться. Злоумышленник может в этом окне перенаправить имя файла на другой файл.
NOTE
Здесь есть важная техническая деталь: хотя сопоставление имени файла с inode изменчиво, сопоставление inode с дескриптором файла стабильно14. Как только вы получили дескриптор файла, он указывает непосредственно на inode и больше не зависит от имени файла. Вот почему CERT SEI рекомендует «открывать критически важные файлы только один раз, а затем выполнять все необходимые операции через дескриптор файла, а не по имени».
Нечувствительность к регистру и нормализация Unicode в macOS усложняют проблему TOCTOU. Когда проверка безопасности использует одно представление имени файла, а фактическая операция с файлом — другое, эквивалентное, но отличное представление, окно TOCTOU расширяется.
В статье USENIX FAST’23 «Unsafe at Any Copy: Name Collisions from Mixing Case Sensitivity» эта проблема была изучена систематически15. Исследование показало, что правила преобразования регистра и технологии нормализации в разных файловых системах различаются. Например, temp_200K (где K — знак Кельвина, U+212A) и temp_200k считаются идентичными в NTFS и APFS, но разными в ZFS.
Эта несогласованность является благодатной почвой для уязвимостей.
Эшелонированная защита: когда именам файлов нельзя доверять
Столкнувшись с реальностью того, что именам файлов нельзя доверять, Apple выбрала стратегию эшелонированной защиты (Defense in Depth).
Основная идея этой стратегии: раз мы не можем сделать имена файлов надежными, не стоит полагаться на них для установления доверия. Вместо этого мы создаем независимые барьеры безопасности на нескольких уровнях, каждый из которых использует свою основу доверия.
Давайте посмотрим, как Apple реализует эту стратегию.
Деревья Меркла и подписанный системный том
В macOS Big Sur (11.0) был введен механизм подписанного системного тома (Signed System Volume, SSV)16. Основная идея SSV: использовать криптографические хеши для проверки целостности системы вместо того, чтобы полагаться на имена файлов.
Техническая реализация SSV основана на деревьях Меркла. Дерево Меркла — это элегантная структура данных, которая позволяет нам проверять целостность набора данных любого размера с помощью одного «корневого хеша» фиксированного размера17.
Принцип работы дерева Меркла следующий:
- Данные разбиваются на блоки, вычисляется хеш каждого блока (листовые узлы).
- Соседние хеши объединяются в пары, вычисляется их комбинированный хеш (внутренние узлы).
- Шаг 2 повторяется до тех пор, пока не останется один хеш (корневой узел).
Эта структура обладает важным свойством: изменение любого блока данных приведет к изменению всех хешей на пути от этого листового узла к корню. Таким образом, если корневой хеш доверенный, мы можем проверить целостность всего набора данных.
TIP
Эффективность проверки в дереве Меркла логарифмическая.
Чтобы проверить целостность конкретного блока данных, нужно проверить только хеши на пути от этого листа к корню — для блоков данных это требует всего вычислений хеша. Это позволяет SSV быстро проверять целостность системы при загрузке, не увеличивая время запуска значительно.
В реализации SSV каждый файл на системном томе имеет хеш SHA-256, хранящийся в метаданных файловой системы. Хеш корневого узла называется «печатью» (seal), и он охватывает каждый байт на SSV.
Эта печать проверяется загрузчиком при каждом запуске Mac. Если проверка не удается, загрузка прерывается, и пользователю предлагается переустановить операционную систему18.
Что это означает? Какие бы манипуляции с регистром или вариантами Unicode ни использовал злоумышленник, даже если он получит права Root, любая попытка изменить содержимое системного тома приведет к несовпадению хеша, и система откажется загружаться. Проблема семантической интерпретации имен файлов здесь становится неважной — целостность всего тома гарантируется криптографическим хешем, а не именем файла.
Метки метаданных и SIP
До появления SSV в macOS уже существовала технология SIP (System Integrity Protection), также известная как «rootless»19. Основная идея SIP: использовать метки метаданных для защиты критических файлов вместо того, чтобы полагаться на их имена.
SIP ввела новый флаг файла restricted. Файлы, помеченные как restricted, не могут быть изменены даже пользователем root20.
Ключевой момент: проверка SIP основана на метках метаданных inode, а не на именах файлов. Когда любой процесс пытается записать данные в защищенный каталог, ядро проверяет, помечен ли этот inode как «защищенный SIP». Даже если злоумышленник обманет проверки верхнего уровня с помощью вариантов Unicode или манипуляций с регистром, когда запрос дойдет до ядра, ядро увидит флаг restricted у inode и отклонит операцию.
Это важный принцип проектирования: перенос основы доверия с имени файла на метаданные inode. Имя файла можно подделать, но метаданные inode управляются непосредственно ядром и не зависят от семантической интерпретации имени.
Дескрипторы файлов: в обход имен
В документации для разработчиков Apple рекомендует использовать NSURL (как объект) и дескрипторы файлов (File Descriptor) вместо прямого использования строк путей к файлам21.
За этим выбором стоят глубокие соображения безопасности.
В механизме Sandbox, когда пользователь разрешает приложению доступ к определенному файлу, система выдает приложению токен, а не путь. Приложение может использовать этот токен для запроса доступа к файлу у ядра, а ядро находит файл через 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 или получить доступ к доверенным системным процессам.
Размышления о философии дизайна
Возвращаясь к критике Линуса Торвальдса, мы видим, что это не только технический вопрос, но и вопрос философии дизайна.
Философия дизайна Unix подчеркивает простоту и ортогональность. Имена файлов — это последовательности байтов, ядро не интерпретирует их значение. Преимущества такого дизайна — предсказуемость и безопасность: никаких скрытых семантических преобразований, никаких неожиданных отношений эквивалентности.
macOS выбрала другой путь, стремясь обеспечить более дружелюбный пользовательский интерфейс. Нечувствительность к регистру избавляет пользователя от необходимости беспокоиться о разнице между Document.txt и document.txt. Нормализация Unicode избавляет пользователя от необходимости понимать разницу между NFD и NFC.
Но за это дружелюбие приходится платить. Когда файловая система начинает «понимать» имена файлов, она берет на себя ответственность за определение того, что является «одинаковым». А определение «одинаковости» сложно, культурно зависимо и постоянно эволюционирует.
Более глубокая проблема заключается в том, что когда мы используем разные определения «одинаковости» на разных уровнях системы, возникают уязвимости безопасности. Программа проверки безопасности может использовать одно отношение эквивалентности, файловая система — другое, и злоумышленники используют именно это несоответствие для обхода проверок.
Стратегия эшелонированной защиты Apple — это прагматичный компромисс. Поскольку невозможно изменить исторические решения (нечувствительность к регистру уже стала поведением по умолчанию в macOS), независимые барьеры безопасности строятся на более высоких и более низких уровнях:
- SSV защищает целостность системы с помощью криптографических хешей.
- SIP защищает критические файлы с помощью меток метаданных.
- Дескрипторы файлов обходят имена файлов, работая напрямую с inode.
- TCC защищает пользовательские данные через аутентификацию процессов.
Общая черта этих механизмов: они не доверяют именам файлов. Они используют криптографические хеши, метки метаданных, идентификацию процессов и inode для установления доверия, а не полагаются на строки, которые могут быть искажены.
Заключение
Критика Торвальдса напоминает нам об основополагающем принципе: системы безопасности не должны полагаться на сложную семантическую интерпретацию.
Простая модель, даже если она несовершенна, часто оказывается более надежной и предсказуемой, чем сложная. Принцип Unix «имя файла — это последовательность байтов» — именно такая простая модель: она отказывается от «понимания» имен файлов, но взамен дает предсказуемость и безопасность.
История macOS доказывает цену сложности. От нормализации Unicode в HFS+ до нечувствительности к нормализации в APFS, от уязвимостей Git до атак TOCTOU — семантическая интерпретация имен файлов всегда была рассадником проблем безопасности.
Но стратегия Apple также демонстрирует инженерную мудрость: когда мы не можем устранить сложность, мы можем ограничить ее влияние с помощью эшелонированной защиты. SSV, SIP, дескрипторы файлов, TCC — все эти механизмы не зависят от семантической интерпретации имен файлов, они устанавливают независимую основу доверия на более низком уровне.
Для разработчиков уроки этой истории очевидны:
- Никогда не предполагайте, что имя файла уникально или неизменно.
- Используйте дескрипторы файлов вместо строк путей для операций с файлами.
- При выполнении проверок безопасности учитывайте влияние регистра и нормализации Unicode.
- При кроссплатформенной разработке тестируйте код как в чувствительных, так и в нечувствительных к регистру средах.
Как сказал Торвальдс, имена файлов должны быть просто последовательностью байтов. Когда мы начинаем наделять их магическим смыслом, мы открываем ящик Пандоры.
Список литературы
Footnotes
-
Phoronix. “Linus Torvalds Expresses His Hatred For Case-Insensitive File-Systems.” 2025. https://www.phoronix.com/news/Linus-Torvalds-Anti-Case-Fold ↩
-
Bovet, D. P., & Cesati, M. “Understanding the Linux Kernel, Second Edition.” O’Reilly Media, 2002. ↩
-
Linux Kernel Documentation. “Overview of the Linux Virtual File System.” https://docs.kernel.org/filesystems/vfs.html ↩
-
I18n Guy. “Internationalization for Turkish: Dotted and Dotless Letter I.” http://www.i18nguy.com/unicode/turkish-i18n.html ↩
-
Atwood, J. “What’s Wrong With Turkey?” Coding Horror, 2008. https://blog.codinghorror.com/whats-wrong-with-turkey/ ↩
-
Unicode Consortium. “UAX #15: Unicode Normalization Forms.” https://unicode.org/reports/tr15/ ↩
-
Apple Developer. “Technical Q&A QA1235: Converting to Precomposed Unicode.” https://developer.apple.com/library/archive/qa/qa1235/_index.html ↩
-
Wikipedia. “HFS Plus.” https://en.wikipedia.org/wiki/HFS_Plus ↩
-
Eclectic Light. “Explainer: Unicode, normalization and APFS.” 2021. https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/ ↩
-
InfoQ. “Analyzing Git Clone Vulnerability.” 2021. https://www.infoq.com/news/2021/03/git-clone-vulnerability/ ↩
-
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 ↩
-
CERT/CC. “VU#999008 - Compilers permit Unicode control and homoglyph characters.” 2021. https://www.kb.cert.org/vuls/id/999008 ↩
-
MITRE. “CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition.” https://cwe.mitre.org/data/definitions/367.html ↩
-
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 ↩
-
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 ↩
-
Apple Support. “Signed system volume security.” Apple Platform Security Guide. https://support.apple.com/guide/security/signed-system-volume-security-secd698747c9/web ↩
-
Wikipedia. “Merkle tree.” https://en.wikipedia.org/wiki/Merkle_tree ↩
-
Jamf Blog. “What’s New in macOS Big Sur Security.” 2020. https://www.jamf.com/blog/whats-new-in-macos-big-sur-security/ ↩
-
Wikipedia. “System Integrity Protection.” https://en.wikipedia.org/wiki/System_Integrity_Protection ↩
-
Apple Support. “System Integrity Protection.” Apple Platform Security Guide. https://support.apple.com/guide/security/system-integrity-protection-secb7ea06b49/web ↩
-
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 ↩
-
Rainforest QA. “A deep dive into macOS TCC.db.” 2021. https://www.rainforestqa.com/blog/macos-tcc-db-deep-dive ↩
-
Huntress. “Full Transparency: Controlling Apple’s TCC.” 2024. https://www.huntress.com/blog/full-transparency-controlling-apples-tcc ↩