Хабрахабр

[Перевод] Последствия переписывания компонентов Firefox на Rust

В прошлых статьях цикла мы обсудили безопасность памяти и безопасность потоков в Rust. В этой последней статье посмотрим на последствия реального применения Rust на примере проекта Quantum CSS.

Это нисходящий процесс, который спускается по дереву DOM, после расчёта родительского CSS дочерние стили можно вычислять независимо: идеальный вариант для параллельных вычислений. Движок CSS применяет правила CSS на странице. Обе провалились. К 2017 году Mozilla предприняла две попытки распараллелить систему стилей с помощью C++.

Улучшение безопасности — просто удачный побочный эффект. Разработка Quantum CSS началась, чтобы повысить производительность.

Между защитой памяти и багами информационной безопасности есть определённая связь. Поэтому мы ожидали, что применение Rust уменьшит поверхность атаки в Firefox. В этой статье рассмотрим потенциальные уязвимости, которые выявили в движке CSS с момента первоначального выпуска Firefox в 2002 году. Затем посмотрим на то, что можно и нельзя было предотвратить с помощью Rust.

Если бы у нас была машина времени и мы могли с самого начала написать его Rust, то 51 (73,9%) ошибка стала бы невозможной. За всё время в CSS-компоненте Firefox обнаружено 69 ошибок безопасности. Хотя 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, либо, в случае пользовательских свойств, значением, которое начинается с eCSSProperty_COUNT (общее количество некастомных свойств CSS). Порядок всех свойств CSS (в том числе кастомных, то есть пользовательских) хранится в массиве mOrder. Чтобы получить имя пользовательских свойства, сначала необходимо получить значение из 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

Соответствующий код 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 динамический размер. Однако в реализации вектора стандартной библиотеки встроена проверка границ. При появлении недопустимого индекса программа немедленно завершается контролируемым образом, предотвращая любой несанкционированный доступ.

Например, мы используем мощные встроенные структуры данных Rust для унификации порядка расположения и имён свойств. Реальный код в Quantum CSS использует очень разные структуры данных, поэтому точного эквивалента нет. Структуры данных Rust также улучшают инкапсуляцию данных и уменьшают вероятность таких логических ошибок. Это избавляет от необходимости поддерживать два независимых массива. Но она всё равно даёт все гарантии безопасности, обеспечивая при этом более понятную абстракцию базовых данных. Поскольку код должен взаимодействовать с кодом C++ в других частях браузера, новая функция GetCustomPropertyNameAt не выглядит как идиоматический код Rust.

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

Тем не менее, Rust снимает с наших плеч груз безопасности памяти, позволяя сосредоточиться на логической корректности кода. Компиляторы не могут выловить все ошибки программистов.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть