Unsafe Rust

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Unsafe Rust
ЯзыкRust
Ключевое словоunsafe (блоки, функции, трейты, impl, extern-блоки)
Пять «суперспособностей»сырые указатели, unsafe-функции, изменяемые static, unsafe-трейты, поля union
Что не отключаетсяпроверка заимствований, типов, времён жизни для обычных ссылок
Инструменты проверкиMiri, санитайзеры, cargo-geiger
СправочникThe Rustonomicon

Unsafe Rust — часть языка Rust, в которой программист обещает компилятору соблюдать инварианты, которые тот не может проверить сам. Обычный («безопасный») Rust гарантирует отсутствие висячих указателей, гонок данных и выхода за границы через систему владения и заимствования. Но некоторые вещи — работа с железом, вызов библиотек на C, реализация эффективных структур данных — принципиально требуют операций, корректность которых компилятор доказать не может. Для них есть ключевое слово unsafe.[1]

Что разрешает unsafe

unsafe не выключает проверки Rust: заимствования, типы и времена жизни обычных ссылок проверяются как всегда. Он лишь открывает пять дополнительных возможностей:[1]

  1. разыменовывать сырые указатели *const T и *mut T;
  2. вызывать unsafe-функции, включая функции из других языков (FFI);
  3. читать и изменять изменяемые статические переменные (static mut);
  4. реализовывать unsafe-трейты (например, Send и Sync вручную);
  5. обращаться к полям union.
let mut x = 10;
let p = &mut x as *mut i32;   // создать сырой указатель можно и в safe-коде
unsafe {
    *p += 1;                  // а разыменовать — только в unsafe
}
assert_eq!(x, 11);

Неопределённое поведение

Если внутри unsafe нарушить правила языка, возникает неопределённое поведение (UB): компилятор вправе оптимизировать код, исходя из того, что нарушения не бывает, и программа может вести себя как угодно — не только «упасть». К UB относятся:[3]

  • разыменование висячего, нулевого или невыровненного указателя;
  • гонка данных;
  • одновременное существование &mut T и другой ссылки на те же данные;
  • создание недопустимого значения: bool, отличного от 0 и 1, неинициализированной памяти как целого числа, невалидного UTF-8 в str;
  • вызов функции с неверным ABI.

Важная мысль Rustonomicon: ошибка может быть в безопасном коде рядом, если он нарушает инвариант, на который опирается unsafe-блок.[2] Поэтому границей «опасной зоны» на практике считается модуль, а не блок.

Безопасные абстракции

Главный приём — инкапсулировать unsafe внутри функции или типа с безопасным интерфейсом, который невозможно использовать неправильно. Вся стандартная библиотека устроена так: Vec, String, HashMap, Rc, Mutex внутри используют сырые указатели и ручное управление памятью, но снаружи безопасны.

pub fn split_at_mut<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = s.len();
    let ptr = s.as_mut_ptr();
    assert!(mid <= len);            // проверка делает unsafe-код корректным
    // SAFETY: части не пересекаются, обе лежат внутри исходного среза.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

Принятые соглашения:[7]

  • каждый unsafe-блок сопровождается комментарием // SAFETY: с объяснением, почему он корректен;
  • у каждой unsafe fn в документации есть раздел # Safety с условиями, которые должен выполнить вызывающий;
  • линт Clippy undocumented_unsafe_blocks следит за этим (см. Clippy и rustfmt).

В редакции Rust 2024 тело unsafe fn больше не считается одним большим unsafe-блоком: опасные операции внутри неё тоже нужно оборачивать в unsafe { }, а блоки extern пишутся как unsafe extern "C".[5]

FFI

Вызов функций из библиотек на C — самая частая причина использовать unsafe:

unsafe extern "C" {
    fn strlen(s: *const std::ffi::c_char) -> usize;
}

let s = std::ffi::CString::new("привет").unwrap();
let n = unsafe { strlen(s.as_ptr()) };   // 12 байт в UTF-8

Компилятор не знает, что делает чужой код, поэтому ответственность за корректные аргументы лежит на программисте. Обёртки генерируют инструменты bindgen (из заголовков C) и cxx (для C++), а для экспорта Rust в C — cbindgen. Через тот же механизм Rust встраивается в Python (PyO3) и компилируется в WebAssembly.

Сколько unsafe в реальном коде

Исследование 2020 года по крейтам crates.io показало, что около 76 % крейтов не содержат unsafe вовсе, а в остальных он сосредоточен в небольших участках — чаще всего для FFI и оптимизаций.[6] Посчитать unsafe в зависимостях проекта помогает cargo-geiger.

Как проверять

  • Miri — интерпретатор промежуточного представления Rust, который во время выполнения тестов обнаруживает многие виды UB: выход за границы, использование после освобождения, нарушения правил алиасинга:[4]
rustup +nightly component add miri
cargo +nightly miri test
  • Санитайзеры (AddressSanitizer, ThreadSanitizer) через флаги nightly-компилятора.
  • Фаззинг (cargo fuzz) для кода, разбирающего внешние данные.
  • Ревью: unsafe-код проверяют особенно тщательно, многие проекты запрещают его атрибутом #![forbid(unsafe_code)].

Unsafe и безопасность памяти

Наличие unsafe не отменяет преимуществ Rust перед C и C++: потенциально опасный код явно помечен, локализован и поддаётся аудиту, а остальная программа проверяется компилятором. Это противоположность языкам с ручным управлением памятью, где «опасна» любая строка (см. Сборка мусора vs ручное управление памятью).

См. также

Источники

  1. The Rust Programming Language — Unsafe Rust
  2. The Rustonomicon
  3. The Rust Reference — Behavior considered undefined
  4. Miri — an interpreter for Rust's mid-level IR
  5. Rust 2024 Edition Guide — Unsafe extern blocks and unsafe_op_in_unsafe_fn
  6. Astrauskas et al. — How do programmers use unsafe Rust? (OOPSLA 2020)
  7. Rust API Guidelines — Documentation (Safety sections)