Крошечный паттерн безопасного кода, нарушивший важное обещание
Фундаментальное обещание Rust — о том, что borrow checker предотвращает ошибки use-after-free — недавно столкнулось с серьезным вызовом. Issue #164250, открытый на прошлой неделе, описывает ошибку безопасности памяти, воспроизводимую в полностью безопасном коде Rust, что ранее считалось невозможным. Эта уязвимость оставалась незамеченной в течение 11 лет, начиная с Rust 1.4.0 в 2015 году.
Эксплойт основан на специфическом паттерне: обобщенная структура, такая как struct Q<T: 'static>(Option<T>);, объявляет требование времени жизни 'static для своего параметра типа T. Важно отметить, что эта структура также реализует пользовательскую функцию Drop, которая неявно доверяет этому ограничению 'static, предполагая, что содержащиеся данные будут существовать в течение всего времени работы программы.
Удивительный шаг происходит во время присваивания. Разработчики создают пустую структуру, например, let mut q = Q(None);. Затем они присваивают ссылку на локальную переменную с коротким временем жизни, такую как &l, в ее ковариантное поле: q.0 = Some(&l);. Здесь компилятор не обеспечивает соблюдение обязательства по времени жизни 'static внешнего типа для присвоенной ссылки.
Это упущение позволяет реализации Drop получить ссылку, которую она считает 'static, но которая на самом деле указывает на освобожденную память после того, как локальная переменная выходит из области видимости. Этот сценарий, подтвержденный с помощью AddressSanitizer и Miri, приводит к stack-use-after-scope или dangling reference use-after-free, что напрямую нарушает основные гарантии безопасности памяти Rust.
Как обещание времени жизни превратилось в висячую ссылку
Суть сбоя заключается в том, как компилятор Rust обрабатывает обещания времени жизни во время определенных присваиваний. Структура может объявить, что любая ссылка, которую она содержит, должна существовать в течение всего времени работы программы, используя ограничение времени жизни 'static. Пользовательская реализация Drop для этой структуры затем неявно доверяет этому обещанию, предполагая, что сохраненная ссылка останется валидной.
Проблемы возникают, когда ссылка на локальную переменную — с ограниченной областью видимости — присваивается полю внутри такой структуры. Компилятор выполняет проверку типа при присваивании, но критически важно, что он пропускает повторную проверку общего требования к времени жизни структуры по отношению к вновь присвоенной ссылке. Это создает разрыв: деструктор структуры считает, что он содержит ссылку 'static, но базовые данные являются локальной переменной, которая вот-вот истечет.
Когда структура удаляется (dropped), выполняется ее реализация Drop, работающая со ссылкой, которая теперь указывает на освобожденную память. Специально созданный Drop может затем сохранить эту висячую ссылку (dangling reference) или попытаться использовать ее, что приведет к ошибкам памяти use-after-free или stack-use-after-scope. Например, Drop может передать невалидную ссылку в отдельный поток для последующего доступа.
Хотя пример паттерна не включает блоки unsafe напрямую, результирующее повреждение памяти демонстрирует дыру в проверках компилятора. Инструменты, такие как Miri и AddressSanitizer (ASan), могут обнаруживать эти невалидные обращения к памяти, помечая такие проблемы, как stack-use-after-scope или dangling reference use-after-free, подтверждая возможность эксплуатации Issue #164250.
Одиннадцать лет — и сложно наткнуться случайно
Участник разработки компилятора Rust проследил ошибку безопасности памяти в Issue #164250 до Rust 1.4, выпущенного в октябре 2015 года. Это означает, что уязвимости около 11 лет — поразительное долголетие для ошибки в языке, чьим главным обещанием является безопасность памяти. Это открытие заставило автора отчета воскликнуть: «Как, черт возьми, это не было обнаружено раньше?»
Несмотря на возраст и влияние на фундаментальные гарантии Rust, проблема классифицирована как I-unsound и P-high, а не P-critical. Эта классификация отражает искусственный характер паттерна эксплойта, а не преуменьшение важности корректности (soundness). Рейтинг P-critical обычно резервируется для ошибок, которые легко вызвать случайно в обычном коде.
Случайное столкновение с этой ошибкой в обычном коде приложений считается крайне маловероятным. Эксплуатация ошибки требует преднамеренного сочетания факторов:
- Обобщенный тип с ограничением времени жизни
'static - Разрешающее присваивание поля, которое обходит проверку времени жизни
- Пользовательская реализация
Drop, разработанная для использования несоответствия, часто путем перемещения висячей ссылки в глобальную переменную или другой поток.
Участник команды Rust Types Team lcnr отметил, что 11-летняя «спячка» ошибки говорит о том, что на нее нелегко наткнуться. Этот специфический паттерн, хотя и нарушает основной инвариант, требует вредоносной или крайне необычной реализации Drop для вызова use-after-free. Более подробную техническую информацию см. в GitHub issue: struct's lifetime requirements can be bypassed via covariant field assignment · Issue #164250 · rust-lang/rust.
Нравится статья? Получайте такие каждое утро на почту.
одно письмо в день · отписка в два клика · без сторонних трекеров
Исправление — и урок, стоящий за заголовком
Предложенное решение для Issue #164250 быстро появилось в PR #164277. Исправление включает в себя улучшение проверки корректности (well-formedness) компилятора для MIR local types, в частности, обеспечение того, чтобы присваивания и неявное подтипирование не обходили критические проверки ограничений времени жизни. Это напрямую устраняет отсутствие проверки, которое позволяло нарушить обещание времени жизни 'static.
Этот инцидент, хотя и тревожный, подчеркивает более широкую истину: безопасность памяти, даже в таком языке, как Rust, фундаментально опирается на гарантии, обеспечиваемые компилятором. Компиляторы — это сложные программные системы, и, как и любой сложный код, они могут содержать тонкие граничные случаи и непредвиденные взаимодействия. Тщательное тестирование, непрерывный обзор кода и постоянная работа над корректностью (soundness) незаменимы.
Основное обещание Rust о безопасности памяти в безопасном коде остается незыблемым, но эта 11-летняя ошибка служит суровым напоминанием. Она подчеркивает, почему корректность компилятора так же важна, как и проектирование языка, для выполнения этого обещания. Хотя это реальное нарушение гарантии безопасного кода Rust, это не означает, что Rust в целом небезопасен; скорее, это подтверждает необходимость бдительной разработки компилятора.
Часто задаваемые вопросы
Что это была за ошибка безопасности памяти в Rust?
Issue #164250 описывает, как присваивание поля могло обойти требование времени жизни в структуре, позволяя безопасному коду создать висячую ссылку при использовании специально созданного деструктора.
Может ли обычный код на Rust случайно вызвать эту ошибку?
Продемонстрированный паттерн требует необычного сочетания, особенно реализации Drop, которая преднамеренно доверяет и использует ссылку с ложно завышенным временем жизни.
Означает ли эта ошибка, что Rust больше не является безопасным с точки зрения памяти?
Нет. Это серьезная ошибка корректности компилятора, но она не отменяет более широкие преимущества безопасности памяти Rust и не означает, что типичные программы на Rust подвержены риску.
Как было предложено исправить ошибку в Rust?
Предложенное исправление проверяет, что локальные типы MIR удовлетворяют требованиям корректности, предотвращая обход ограничений времени жизни при присваиваниях.

