Безопасность памяти для веб-шрифтов

Доминик Рёттшес
Dominik Röttsches
Род Шитер
Rod Sheeter
Чад Брокоу
Chad Brokaw

Опубликовано: 19 марта 2025 г.

Skrifa написана на Rust и создана как замена FreeType, чтобы сделать обработку шрифтов в Chrome безопасной для всех наших пользователей. Skrifa использует преимущества безопасности памяти Rust и позволяет нам быстрее совершенствовать технологию шрифтов в Chrome. Переход от FreeType к Skrifa позволяет нам быть гибкими и уверенными в своих действиях при внесении изменений в код шрифтов. Теперь мы тратим гораздо меньше времени на исправление ошибок безопасности, что приводит к более быстрым обновлениям и повышению качества кода.

В этой статье рассказывается о причинах отказа Chrome от FreeType, а также приводятся некоторые интересные технические подробности улучшений, которые стали возможны благодаря этому шагу.

Почему стоит заменить FreeType?

Уникальность интернета заключается в том, что он позволяет пользователям получать доступ к ненадежным ресурсам из самых разных ненадежных источников, рассчитывая на то, что все будет работать без проблем и безопасно. Это предположение, как правило, верно, но выполнение этого обещания имеет свою цену. Например, для безопасного использования веб-шрифта (шрифта, передаваемого по сети) Chrome использует несколько мер безопасности:

Chrome поставляется с FreeType и использует его в качестве основной библиотеки для обработки шрифтов на Android, ChromeOS и Linux. Это означает, что многие пользователи могут оказаться уязвимыми в случае обнаружения уязвимости в FreeType.

Библиотека FreeType используется Chrome для вычисления метрик и загрузки контурных подсказок из шрифтов. В целом, использование FreeType стало огромным успехом для Google. Она выполняет сложную работу, и делает это хорошо, мы активно на неё полагаемся и вносим свой вклад. Однако она написана на небезопасном коде и возникла во времена, когда вероятность вредоносных входных данных была ниже. Простое отслеживание потока проблем, обнаруженных с помощью фаззинга, обходится Google как минимум в 0,25 штатных инженеров-программистов. Хуже того, мы, как видно, находим не всё или находим проблемы только после того, как код уже предоставлен пользователям.

Такая картина проблем не уникальна для FreeType; мы наблюдаем, что и другие небезопасные библиотеки допускают наличие проблем, даже когда мы привлекаем лучших инженеров-программистов, проводим проверку кода после каждого изменения и требуем тестирования.

Почему проблемы постоянно возникают

При оценке безопасности FreeType мы выявили три основных класса проблем (список не является исчерпывающим):

Использование нецензурной лексики

Шаблон/Выпуск Пример
Ручное управление памятью
Неконтролируемый доступ к массиву CVE-2022-27404
Переполнение целочисленного типа В процессе выполнения встроенных виртуальных машин для хинтинга TrueType при отрисовке CFF и хинтинге
https://issues.oss-fuzz.com/issues?q=FreeType%20Integer-overflow
Неправильное использование распределения с нулевым или ненулевым обнулением. Обсуждение в https://gitlab.freedesktop.org/freetype/freetype/-/merge_requests/94 , после этого обнаружено 8 проблем с фаззером.
Неверные приведения типов См. следующую строку об использовании макросов.

Вопросы, специфичные для проекта

Шаблон/Выпуск Пример
Макросы скрывают отсутствие явного указания размера.
  • Макросы, такие как FT_READ_* и FT_PEEK_* скрывают используемые целочисленные типы, показывая, что типы C99 с явно указанными размерами (int16_t и т. д.) не используются.
Новый код постоянно добавляет ошибки, даже если он написан с учетом мер по защите от ошибок.
  • Поддержка COLRv1 и OT-SVG решает обе проблемы.
  • Фаззинг обнаруживает некоторые, но не обязательно все ошибки #32421 и #52404.
Недостаток тестов
  • Создание тестовых шрифтов — трудоемкий и сложный процесс.

Проблемы с зависимостями

Фаззинг неоднократно выявлял проблемы в библиотеках, от которых зависит FreeType, таких как bzip2, libpng и zlib. В качестве примера сравните freetype_bdf_fuzzer: Use-of-uninitialized-value in inflate .

Фаззинга недостаточно.

Фаззинг — автоматизированное тестирование с широким спектром входных данных, включая случайные некорректные, — предназначен для выявления многих типов проблем, которые попадают в стабильную версию Chrome. Мы проводим фаззинг FreeType в рамках проекта Google oss-fuzz . Он действительно выявляет проблемы, но шрифты оказались несколько устойчивыми к фаззингу по следующим причинам.

Файлы шрифтов сложны, сравнимы с видеофайлами, поскольку содержат множество различных типов информации. Файлы шрифтов представляют собой контейнерный формат для множества таблиц, где каждая таблица выполняет свою функцию, обрабатывая текст и шрифты вместе для создания правильно расположенного глифа на экране. В файле шрифта вы найдете:

  • Статические метаданные, такие как названия шрифтов и параметры для вариативных шрифтов.
  • Сопоставление символов Юникода с глифами.
  • Сложный свод правил и грамматика для размещения глифов на экране.
  • Визуальная информация: формы глифов и информация об изображении, описывающая внешний вид глифов, размещенных на экране.
    • В свою очередь, визуальные таблицы могут включать программы хинтинга TrueType, представляющие собой мини-программы, выполняемые для изменения формы глифа.
    • Строковые символы в таблицах CFF или CFF2, представляющие собой обязательные инструкции по отрисовке кривых и хинтингу, выполняемые в механизме рендеринга CFF.

Сложность файлов шрифтов сравнима со сложностью собственного языка программирования и обработки данных конечным автоматом, что требует наличия специальных виртуальных машин для их выполнения.

Из-за сложности формата метод фаззинга имеет недостатки при поиске проблем в файлах шрифтов.

Достичь хорошего покрытия кода или прогресса в фаззере сложно по следующим причинам:

  • При фаззинге программ, использующих хинтинги TrueType, строковые представления символов CFF и компоновку OpenType с помощью простых мутаторов, основанных на изменении битов/сдвиге/вставке/удалении, сложно достичь всех комбинаций состояний.
  • Для фаззинга необходимо, чтобы структуры кода были хотя бы частично корректными. Случайные мутации редко это делают, что затрудняет достижение хорошего покрытия кода, особенно на более глубоких уровнях.
  • В текущих разработках ClusterFuzz и oss-fuzz для фаззинга пока не используется мутация, учитывающая структуру. Использование мутаторов, учитывающих грамматику или структуру, может помочь избежать появления вариантов, которые отклоняются на ранних стадиях, но при этом увеличится время разработки и появятся ошибки, которые могут привести к пропуску частей пространства поиска.

Для успешного фаззинга данные в нескольких таблицах должны быть синхронизированы:

  • Обычные алгоритмы мутации, используемые фаззерами, не позволяют получить частично корректные данные, поэтому многие итерации отклоняются, и прогресс замедляется.
  • Сопоставление глифов, таблицы компоновки OpenType и отрисовка глифов взаимосвязаны и зависят друг от друга, образуя комбинаторное пространство, до границ которого трудно добраться с помощью фаззинга.
  • Например, на обнаружение уязвимости COLRv1 tt_face_get_paint, представляющей серьезную угрозу безопасности, ушло более 10 месяцев.

Несмотря на все наши усилия, проблемы с безопасностью шрифтов неоднократно достигали конечных пользователей. Замена FreeType на альтернативу на Rust предотвратит возникновение множества целых классов уязвимостей.

Скрифа в Chrome

Skia — это графическая библиотека, используемая Chrome. Skia использует FreeType для загрузки метаданных и форм букв из шрифтов. Skrifa — это библиотека на Rust, входящая в семейство библиотек Fontations , которая предоставляет безопасную замену тем частям FreeType, которые используются Skia.

Для перехода с FreeType на Skia команда Chrome разработала новый бэкэнд шрифтов Skia на основе Skrifa и постепенно внедряла это изменение для пользователей:

  • В Chrome 128 (август 2024 г.) мы включили Fontations для использования в менее распространенных форматах шрифтов, таких как цветные шрифты и CFF2, в качестве безопасного пробного запуска.
  • В Chrome 133 (февраль 2025 г.) мы включили Fontations для использования всех веб-шрифтов в Linux, Android и ChromeOS, а также для использования веб-шрифтов в качестве резервного варианта в Windows и Mac — в случаях, когда система не поддерживает определенный формат шрифта, но Chrome необходимо его отображать.

Для интеграции в Chrome мы используем плавную интеграцию Rust в кодовую базу, разработанную командой безопасности Chrome .

В будущем мы перейдем на Fontations и для шрифтов операционных систем, начиная с Linux и ChromeOS, а затем и на Android.

Безопасность превыше всего

Наша главная цель — уменьшить (или, в идеале, устранить!) уязвимости безопасности, вызванные выходом за пределы допустимого диапазона доступа к памяти. Rust предоставляет такую ​​возможность «из коробки», если вы избегаете небезопасных блоков кода.

Для достижения наилучших показателей производительности нам необходимо выполнить одну операцию, которая в настоящее время небезопасна: переинтерпретацию произвольных байтов как строго типизированной структуры данных. Это позволяет нам считывать данные из файла шрифта без выполнения ненужных копирований и имеет решающее значение для создания быстрого парсера шрифтов.

Чтобы избежать использования собственного небезопасного кода, мы решили передать эту ответственность библиотеке bytemuck , разработанной специально для этой цели и широко протестированной и используемой в экосистеме Rust. Концентрация переинтерпретации необработанных данных в bytemuck гарантирует наличие этой функциональности в одном месте и её проверку, а также позволяет избежать повторного использования небезопасного кода. Проект Safe Transmute стремится интегрировать эту функциональность непосредственно в компилятор Rust, и мы перейдём на неё, как только она станет доступна.

Правильность имеет значение

Skrifa построена из независимых компонентов, где большинство структур данных спроектированы как неизменяемые. Это улучшает читаемость, удобство сопровождения и многопоточность. Это также делает код более подходящим для модульного тестирования. Мы воспользовались этой возможностью и разработали набор из примерно 700 модульных тестов, охватывающих весь наш стек, от низкоуровневых процедур парсинга до высокоуровневых виртуальных машин для подсказок.

Корректность также подразумевает точность, а FreeType высоко ценится за генерацию высококачественных контуров. Мы должны соответствовать этому качеству, чтобы стать достойной заменой. С этой целью мы разработали специальный инструмент под названием fauntlet , который сравнивает результаты работы Skrifa и FreeType для групп файлов шрифтов в широком диапазоне конфигураций. Это дает нам некоторую уверенность в том, что мы сможем избежать ухудшения качества.

Кроме того, перед интеграцией в Chromium мы провели широкий спектр сравнений пикселей в Skia, сопоставляя рендеринг FreeType с рендерингом Skrifa и Skia, чтобы убедиться, что различия в пикселях абсолютно минимальны во всех необходимых режимах рендеринга (в различных режимах сглаживания и хинтинга).

Фаззинг-тестирование — важный инструмент для определения того, как программное обеспечение будет реагировать на некорректные и вредоносные входные данные. Мы непрерывно фаззингируем наш новый код с июня 2024 года. Это охватывает как сами библиотеки Rust, так и интеграционный код. Хотя фаззер обнаружил (на момент написания этой статьи) 39 ошибок, стоит отметить, что ни одна из них не является критической с точки зрения безопасности . Они могут вызывать нежелательные визуальные результаты или даже контролируемые сбои, но не приведут к созданию уязвимостей, которые можно использовать для взлома.

Вперёд!

Мы очень довольны результатами наших усилий по использованию Rust для текстового кода. Предоставление пользователям более безопасного кода и повышение производительности разработчиков — это огромный успех для нас. Мы планируем и дальше искать возможности использования Rust в наших текстовых стеках. Если вы хотите узнать больше, Oxidize рассказывает о некоторых планах Google Fonts на будущее.