[Перевод] Последствия переписывания компонентов Firefox на Rust
В прошлых статьях цикла мы обсудили безопасность памяти и безопасность потоков в Rust. В этой последней статье посмотрим на последствия реального применения Rust на примере проекта Quantum CSS.
Движок CSS применяет правила CSS на странице. Это нисходящий процесс, который спускается по дереву DOM, после расчёта родительского CSS дочерние стили можно вычислять независимо: идеальный вариант для параллельных вычислений. К 2017 году Mozilla предприняла две попытки распараллелить систему стилей с помощью C++. Обе провалились.
Разработка Quantum CSS началась, чтобы повысить производительность. Улучшение безопасности — просто удачный побочный эффект.
Между защитой памяти и багами информационной безопасности есть определённая связь. Поэтому мы ожидали, что применение Rust уменьшит поверхность атаки в Firefox. В этой статье рассмотрим потенциальные уязвимости, которые выявили в движке CSS с момента первоначального выпуска Firefox в 2002 году. Затем я посмотрим на то, что можно и нельзя было предотвратить с помощью Rust.
За всё время в CSS-компоненте Firefox обнаружено 69 ошибок безопасности. Если бы у нас была машина времени и мы могли с самого начала написать его Rust, то 51 (73,9%) ошибка стала бы невозможной. Хотя Rust упрощает написание хорошего кода, он тоже не даёт абсолютной защиты.
Rust — это современный язык системного программирования, безопасный для типов и памяти. Как побочный эффект этих гарантий безопасности, программы Rust также потокобезопасны во время компиляции. Таким образом, Rust особенно хорошо подходит для:
- безопасной обработки ненадёжных входящих данных;
- параллелизма для повышения производительности;
- интеграции отдельных компонентов в существующую кодовую базу.
Тем не менее, Rust явно не исправляет некоторые классы ошибок, особенно ошибки корректности. На самом деле, когда наши инженеры переписывали Quantum CSS, они случайно повторили критическую ошибку безопасности, которая ранее была исправлена в коде C++, они случайно удалили исправление бага 641731, который допускает утечку глобальной истории через SVG. Ошибку зарегистрировали заново как баг 1420001. Утечка истории оценивается как критическая уязвимость безопасности. Первоначальное исправление предсталяло собой дополнительную проверку, является ли документ SVG изображением. К сожалению, эту проверку упустили при переписывании кода.
Хотя автоматизированные тесты должны находить нарушения правила :visited
вроде такого, на практике они не обнаружили эту ошибку. Чтобы ускорить автоматические тесты, мы временно отключили механизм, который тестировал эту функцию — тесты не особенно полезны, если они не выполняются. Риск повторной реализации логических ошибок можно уменьшить за счёт хорошего покрытия тестами. Но по-прежнему существует опасность появления новых логических ошибок.
По мере того, как разработчик знакомится с Rust, его код становится ещё более безопасным. Хотя Rust не предотвратит все возможные уязвимости, он устраняет целый класс самых серьёзных багов.
В целом, по умолчанию Rust предотвращает появление ошибок, связанных с памятью, границами, нулевыми/неинициализированными переменными и целочисленным переполнением. Нестандартный баг, упомянутый выше, остаётся возможным: происходит сбой из-за неудавшегося распределения памяти.
Ошибки безопасности по категориям
- Память: 32
- Границы: 12
- Реализация: 12
- Null: 7
- Переполнение стека: 3
- Целочисленное переполнение: 2
- Другое: 1
В нашем анализе все баги связаны с безопасностью, но только 43 получили официальную оценку (её присваивают инженеры Mozilla по безопасности на основе квалифицированных предположений об «эксплуатируемости»). Обычные баги могут указывать на отсутствующие функции или какие-то сбои, которые необязательно приводят к утечке данных или изменению поведения. Официальные ошибки безопасности варьируются от низкой важности (если есть сильное ограничение на поверхности атаки) до критической уязвимости (может позволить злоумышленнику запускать произвольный код на платформе пользователя).
Уязвимости памяти часто классифицируются как серьёзные проблемы безопасности. Из 34 критических/серьёзных проблем 32 были связаны с памятью.
Распределение багов безопасности по серьёзности
- Всего: 70
- Ошибки безопасности: 43
- Критические/серьёзные: 34
- Исправлены Rust: 32
Баг 955913 — переполнение буфера кучи в функции GetCustomPropertyNameAt
. Код использовал неправильную переменную для индексирования, что привело к интерпретации памяти после окончания массива. Это может вызвать сбой при доступе к плохому указателю или копирование памяти в строку, которая передаётся другому компоненту.
Порядок всех свойств CSS (в том числе кастомных, то есть пользовательских) хранится в массиве mOrder
. Каждый элемент представлен либо значением свойства CSS, либо, в случае пользовательских свойств, значением, которое начинается с eCSSProperty_COUNT
(общее количество некастомных свойств CSS). Чтобы получить имя пользовательских свойства, сначала необходимо получить значение из mOrder
, а затем получить доступ к имени в соответствующем индексе массива mVariableOrder
, который хранит имена кастомных свойств по порядку.
Уязвимый код C++:
void GetCustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const {
MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT);
aResult.Truncate();
aResult.AppendLiteral("var-");
aResult.Append(mVariableOrder[aIndex]);
Проблема возникает в строке 6 при использовании aIndex
для доступа к элементу массива mVariableOrder
. Дело в том, что aIndex
должен использоваться с массивом mOrder
, а не mVariableOrder
. Соответствующий элемент для пользовательского свойства, представленного aIndex
в mOrder
, на самом деле mOrder[aIndex] - eCSSProperty_COUNT
.
Исправленный код C++:
void Get CustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const {
MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT);
uint32_t variableIndex = mOrder[aIndex] - eCSSProperty_COUNT;
aResult.Truncate();
aResult.AppendLiteral("var-");
aResult.Append(mVariableOrder[variableIndex]);
}
Соответствующий код Rust
Хотя Rust в некотором роде похож на C++, но использует другие абстракции и структуры данных. Код Rust будет сильно отличаться от C++ (подробнее см. ниже). Во-первых, давайте рассмотрим, что произойдёт, если перевести уязвимый код как можно более буквально:
fn GetCustomPropertyNameAt(&self, aIndex: usize) -> String {
assert!(self.mOrder[aIndex] >= self.eCSSProperty_COUNT);
let mut result = "var-".to_string();
result += &self.mVariableOrder[aIndex];
result
}
Компилятор Rust примет такой код, потому что длину векторов невозможно определить до выполнения. В отличие от массивов, длина которых должна быть известна, у типа Vec в Rust динамический размер. Однако в реализации вектора стандартной библиотеки встроена проверка границ. При появлении недопустимого индекса программа немедленно завершается контролируемым образом, предотвращая любой несанкционированный доступ.
Реальный код в Quantum CSS использует очень разные структуры данных, поэтому точного эквивалента нет. Например, мы используем мощные встроенные структуры данных Rust для унификации порядка расположения и имён свойств. Это избавляет от необходимости поддерживать два независимых массива. Структуры данных Rust также улучшают инкапсуляцию данных и уменьшают вероятность таких логических ошибок. Поскольку код должен взаимодействовать с кодом C++ в других частях браузера, новая функция GetCustomPropertyNameAt
не выглядит как идиоматический код Rust. Но она всё равно даёт все гарантии безопасности, обеспечивая при этом более понятную абстракцию базовых данных.
Поскольку уязвимости часто связаны с нарушением безопасности памяти, код Rust должен значительно уменьшить количество критических CVE. Но даже Rust не идеален. Разработчикам по-прежнему нужно отслеживать ошибки корректности и атаки с утечкой данных. Для поддержки безопасных библиотек по-прежнему необходимы код-ревью, тесты и фаззинг.
Компиляторы не могут выловить все ошибки программистов. Тем не менее, Rust снимает с наших плеч груз безопасности памяти, позволяя сосредоточиться на логической корректности кода.