FlawOpen

flawopen.com/Безопасность памяти/Rust unsafe

Остаётся ли блок unsafe в Rust безопасным для памяти?

CWE-787: запись за пределами буфераСправочная страница
Краткий ответ

unsafe не отключает проверки Rust. Он разрешает отдельные операции, условия безопасности которых компилятор не может доказать; программист обязан выполнить эти условия. Поэтому небезопасная абстракция может позволить обычному безопасному коду вызвать неопределённое поведение (UB), даже если вызывающий код не использует unsafe.

Ключевые термины

Операция unsafe:
Операция с условиями безопасности, которые компилятор не может доказать, например разыменование сырого указателя.
Sound-абстракция:
Безопасный API, через который безопасный вызывающий код не может вызвать неопределённое поведение.
size_hint():
Оценка размера итератора для оптимизации; это не точное число элементов и не доказательство безопасности памяти.
Неопределённое поведение (UB):
Поведение программы, которое Rust не определяет; unsafe-код обязан не допустить его вызова безопасным кодом.

Как ломается небезопасная безопасная абстракция

Шаг 1

Безопасный API принимает итератор

Вызывающий передаёт обычный итератор функции без предусловия unsafe.

Шаг 2

Резервирование опирается на подсказку

Функция резервирует только нижнюю границу; она может быть меньше фактического числа элементов.

Шаг 3

Записи выходят за ёмкость

Реализация записывает каждый элемент через сырой указатель, не расширяя выделение и не проверяя ёмкость перед записью.

Шаг 4

Безопасный вызов приводит к 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, а не у вызывающих, соблюдающих его контракт.

Практические правила

Часто задаваемые вопросы

Можно ли полностью запретить unsafe?

#![forbid(unsafe_code)] запрещает синтаксис unsafe в crate, где задан атрибут; это полезно для приложений. Он не охватывает зависимости, а некоторым реализациям, границам FFI и библиотекам unsafe необходим.

Может ли unsafe-код доверять трейту?

Да. unsafe trait документирует обязательства реализации, на которые может полагаться unsafe-код. TrustedLen — нестабильный трейт стандартной библиотеки для итераторов, обещающих точный size hint. Обычный Iterator::size_hint этого не гарантирует.

Подрывает ли это аргументы в пользу Rust?

Нет. Модель Rust сужает область обязательств по безопасности памяти и делает их явными на границах unsafe. Она не отменяет проверку unsafe-кода, зависимостей, контрактов FFI и безопасных API поверх unsafe-реализаций.

Источники

См. также Безопаснее ли strncpy(), чем strcpy()? CWE и CVE: в чём разница? Mass assignment: риски и безопасные списки полей Утечка состояния serverless: изоляция данных запросов PHP == и сравнение magic hash