flawopen.com/Безопасность памяти/Rust unsafe
Остаётся ли блок unsafe в Rust безопасным для памяти?
unsafe не отключает проверки Rust. Он разрешает отдельные операции, условия безопасности которых компилятор не может доказать; программист обязан выполнить эти условия. Поэтому небезопасная абстракция может позволить обычному безопасному коду вызвать неопределённое поведение (UB), даже если вызывающий код не использует unsafe.
Ключевые термины
- Операция unsafe:
- Операция с условиями безопасности, которые компилятор не может доказать, например разыменование сырого указателя.
- Sound-абстракция:
- Безопасный API, через который безопасный вызывающий код не может вызвать неопределённое поведение.
- size_hint():
- Оценка размера итератора для оптимизации; это не точное число элементов и не доказательство безопасности памяти.
- Неопределённое поведение (UB):
- Поведение программы, которое Rust не определяет; unsafe-код обязан не допустить его вызова безопасным кодом.
Как ломается небезопасная безопасная абстракция
Безопасный API принимает итератор
Вызывающий передаёт обычный итератор функции без предусловия unsafe.
Резервирование опирается на подсказку
Функция резервирует только нижнюю границу; она может быть меньше фактического числа элементов.
Записи выходят за ёмкость
Реализация записывает каждый элемент через сырой указатель, не расширяя выделение и не проверяя ёмкость перед записью.
Безопасный вызов приводит к UB
Если элементов больше, чем помещается, безопасный вызов может записать за границы. Используйте Vec::extend или реализацию с безопасным ростом.
// UNSOUND: reserve uses only the lower hint, // then unchecked writes consume every item. fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { let (lower, _) = iter.size_hint(); dst.reserve(lower); let start = dst.len(); let mut written = 0; unsafe { let out = dst.as_mut_ptr().add(start); for item in iter { // UB once writes exceed spare capacity. out.add(written).write(item); written += 1; } dst.set_len(start + written); } }
// SOUND: Vec::extend handles capacity // growth and length updates safely. fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { dst.extend(iter); }
Что именно гарантирует Rust
Точная гарантия такова: безопасный Rust не вызывает неопределённое поведение, если unsafe-код, от которого он зависит, sound. Rust Reference определяет soundness на границе: unsafe-код должен гарантировать, что безопасные клиенты не вызовут UB. Компилятор не проверяет каждое обязательство внутри unsafe-операции; их должны обосновать программисты и проверяющие.
Это важное практическое преимущество перед C и C++: Rust делает операции, чувствительные к памяти, и связанные с ними обязательства заметнее. Проверяющие могут сосредоточиться на границах unsafe и построенных поверх них безопасных абстракциях. К границам относятся блоки unsafe, контракты unsafe-функций, реализации unsafe-трейтов, объявления FFI и unsafe-атрибуты. Они не всегда ограничены небольшим блоком. Пример — CVE-2021-25900 в smallvec: SmallVec::insert_many резервировал память по нижней границе size hint итератора, но записывал больше элементов, чем вмещало выделение; CVSS 9.8.
Критерий — soundness, а не осторожность
API является unsound, если безопасный код может использовать его для вызова неопределённого поведения. Iterator::size_hint предоставляет границы для оптимизации, а не точное число: корректный итератор может выдать больше нижней границы, а ошибочный — нарушить оценку. Документация запрещает использовать эту подсказку, чтобы убрать проверки границ в unsafe-коде. Ошибки безопасного трейта также могут вызвать логические сбои или панику; это не автоматически UB, но unsafe-код обязан сохранять инварианты памяти при неожиданном поведении вызывающего кода.
Последствия unsafe-кода нелокальны: инвариант памяти, установленный в одном месте, может использоваться вышестоящими безопасными API. Если инвариант неверен, дефект находится в абстракции, открывшей безопасный API, а не у вызывающих, соблюдающих его контракт.
Практические правила
- ✓Не считайте безопасные методы трейтов доказательством безопасности памяти. Используйте
size_hint()только для оптимизации; сохраняйте проверки или увеличивайте ёмкость. - ✓Сохраняйте инварианты при панике.
Iterator::next, деструктор или замыкание могут вызвать раскрутку стека. Частично инициализированное состояние должно оставаться корректным и правильно уничтожаться. - ✓Пишите комментарий
// SAFETY:с необходимыми условиями и объяснением, почему они выполняются. Необъяснённый unsafe невозможно проверить. - ✓Ограничивайте блоки. Оборачивание всего цикла ради сокращения повторов увеличивает объём кода для анализа.
- ✓Запускайте Miri на исполняемых тестах. Он обнаруживает некоторые виды UB на пройденных путях, включая выход за границы, use-after-free и неверное aliasing; он не доказывает soundness всего unsafe-кода.
- ✓Проверяйте зависимости.
cargo auditищет известные рекомендации RustSec;cargo geigerпомогает учитывать unsafe. Ни один инструмент не заменяет проверку unsafe-контрактов поставляемого кода.
Часто задаваемые вопросы
#![forbid(unsafe_code)] запрещает синтаксис unsafe в crate, где задан атрибут; это полезно для приложений. Он не охватывает зависимости, а некоторым реализациям, границам FFI и библиотекам unsafe необходим.
Да. unsafe trait документирует обязательства реализации, на которые может полагаться unsafe-код. TrustedLen — нестабильный трейт стандартной библиотеки для итераторов, обещающих точный size hint. Обычный Iterator::size_hint этого не гарантирует.
Нет. Модель Rust сужает область обязательств по безопасности памяти и делает их явными на границах unsafe. Она не отменяет проверку unsafe-кода, зависимостей, контрактов FFI и безопасных API поверх unsafe-реализаций.
Источники
- The Rust Reference — unsafe code and soundness
- Rust Reference — the unsafe keyword and proof obligations
- Rust standard library — Iterator::size_hint contract
- Rustonomicon — working with unsafe
- RustSec Advisory RUSTSEC-2021-0003 — smallvec buffer overflow
- GitHub Security Advisory GHSA-43w2-9j62-hq99 — CVE-2021-25900
- The Rustonomicon — the guide to unsafe Rust
- RustSec Advisory Database
- Miri — an interpreter that detects undefined behaviour
- CWE-787 — Out-of-bounds Write
- Case study: the smallvec overflow (CVE-2021-25900)