Есть ли Undefined Behavior в Rust?
Если вы никогда не сталкивались с Rust-ом, а слышали, что он помогает избежать Undefined Behavior (UB), то отчасти это так. Некоторые делят язык Rust на 2 части: safe и unsafe. Я бы поделил на 4 части: safe, unsafe, const и async. Но нас интересуют safe и unsafe.
Получить UB в Rust-е не сложно, нужно открыть документацию и найти любой метод, помеченный unsafe, например, get_unchecked
у Vec
. Метод позволяет без проверки границ получить значение из динамического массива. А есть ли UB в safe-подмножестве языка? Есть. Он возможен из-за бага (проблемы) в компиляторе Rust, который живет с 2015 года.
Проблема
Рассмотрим следующий код:
fn helper<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }
pub fn make_static<'a, T>(input: &'a T) -> &'static T {
let f: fn(_, &'a T) -> &'static T = helper;
f(&&(), input)
}
fn main() {
let memory = make_static(&vec![0; 1<<20]);
println!("{:?}", memory);
}
Результат в дебаге и релизе различается, но в том и том варианте появляется ошибка Segmentation fault.
Простое объяснение: Ссылка на временный объект становится статической, то есть компилятор считает, что значение по статической ссылке живет до конца программы, но на самом деле временный вектор очищается после вызова make_static
и дальше происходит обращение к освобожденной памяти.
Дисклеймер: Дальше идет техническая часть специфичная для Rust-а.
Небольшой анализ MIR-а
Будем анализировать часть MIR-а в дебаг варианте:
..
let _1: &std::vec::Vec;
let _2: &std::vec::Vec;
let _3: std::vec::Vec;
..
bb2: {
_2 = &_3;
_1 = make_static::>(_2) -> [return: bb3, unwind: bb8];
}
bb3: {
drop(_3) -> [return: bb4, unwind continue];
}
bb4: {
_15 = const _;
_9 = _15 as &[&str] (PointerCoercion(Unsize));
_14 = &_1;
_13 = core::fmt::rt::Argument::<'_>::new_debug::<&Vec>(_14) -> [return: bb5, unwind continue];
}
bb5: {
_12 = [move _13];
_11 = &_12;
_10 = _11 as &[core::fmt::rt::Argument<'_>] (PointerCoercion(Unsize));
_8 = Arguments::<'_>::new_v1(move _9, move _10) -> [return: bb6, unwind continue];
}
bb6: {
_7 = _print(move _8) -> [return: bb7, unwind continue];
}
Основные переменные:
_1 (memory)
— ссылка на массив, которая используется вprintln!
;_2
— ссылка на массив, которая используется при вызовеmake_static
;_3
— временный массив.
В блоке bb2
происходит сам вызов make_static
. В блоке bb3
происходит освождение памяти, выделенной под массив. В последующих блоках происходит преобразования для вывода данных массива в stdout
.
Итог — обращение к освобожденной памяти.
Детальное объяснение
Вспомним, что такое lifetimes. В Rust тип &'a T
означает ссылку на тип T
, которая действительна для времени жизни 'a
. Между временами жизни могут быть отношения. Такие отношения используют механизмы подтипов (Subtyping) и вариантности (Variance). Например, 'a: 'b
(произносится как »'a переживет 'b»), если время жизни 'a
содержит все время жизни 'b
. Ссылки на ссылки &'a &'b T
допустимы, но только если 'b: 'a
, так как время жизни ссылки не должно превышать время жизни ее содержимого.
Существует также самое длинное время жизни 'static
, такое, что 'static: 'a
для любого времени жизни 'a
. Такое преобразование и позволяет получить ошибку.
Контравариантность позволяет передавать аргументы с большим временем жизни, чем требуется функции, что позволяет использовать helper
в типе fn(_, &'a T) -> &'static T
.
На самом деле исходный вариант проблемы был решен. Изначально там был тип
fn(&'static &'a (), &'a T) -> &'static T
, но замена типа первого аргумента на placeholder (нижнее подчеркивает) позволяет пропустить работу type checker-а.
Вместо вывода
Надеюсь, данную ошибку смогут исправить в 2024 году, так как над этим активно ведутся работы. Были предложены следующие варианты решения:
Запретить контрвариантность в функциях;
Замена trait solver-а для реализации автоматического расширения типа
for<'a, 'b> fn(&'a &'b (), &'b T) -> &'a T
доfor<'a, 'b> where<'b:'a, T:'b> fn(&'a &'b (), &'b T) -> &'a T
;
Чтобы самим не попасться на подобную ловушку, советую для вложенных ссылок явно писать ограничения в where
-блоке для времен жизни.