Если вы никогда не сталкивались с 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<i32>;
let _2: &std::vec::Vec<i32>;
let _3: std::vec::Vec<i32>;
..
bb2: {
_2 = &_3;
_1 = make_static::<Vec<i32>>(_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<i32>>(_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
-блоке для времен жизни.
Ссылки
Комментарии (14)
Dmitri-D
21.12.2023 22:54Сыроватый язык. Недавно столкнулся с transmute из non-mut в mut cсылку c другим типом работал в rust 1.53 и никто не замечал проблемы, а в 1.7 перестало вообще вызываться - оптимизатор просто выкидывал код. Без оптимизатора - поведение соответствует 1.53. Понятно что так нельзя, но тоже хотелось бы видеть warning, а лучше error.
Starche
21.12.2023 22:54Transmuting an & to &mut is always Undefined Behavior.
No you can't do it.
No you're not special.
Dmitri-D
21.12.2023 22:54Sorry if you misunderstood my russian. Correct, transmute this way is UB. The point is to have WARNING or ERROR suggesting to fix the issue.
andreymal
21.12.2023 22:54Насколько я вижу, такой ERROR вполне есть:
error: transmuting &T to &mut T is undefined behavior, even if the reference is unused, consider instead using an UnsafeCell --> src/main.rs:3:36 | 3 | let mut b: &mut i32 = unsafe { std::mem::transmute(&a) }; | ^^^^^^^^^^^^^^^^^^^ | = note: `#[deny(mutable_transmutes)]` on by default
Можете показать такой код, в котором ERROR не возникает?
Vlafy2
21.12.2023 22:54Какой мерзкий синтаксис, кто это выдумал? Совсем нечитаемый код.
vibornoff
21.12.2023 22:54Это искусственный пример. В реальном коде почти никогда такого не встречается. Реальный код с активным использованием автовывода типов и вовсе похож на typescript.
Вот, например:
pub(super) async fn feed(config: &FeedConfig, par: usize) -> Result<(), anyhow::Error> { // Local per-feed state let ws_url = config.ws_url; let api_url = config.api_url; let ws_conn_limiter = Arc::new(RateLimiter::direct( Quota::with_period(Duration::from_millis(config.conn_rate_limit_per_ip)).unwrap(), )); let api_conn_limiter = Arc::new(RateLimiter::direct( Quota::with_period(Duration::from_millis(config.conn_rate_limit_per_ip)).unwrap(), )); let ws_conn_timeout = Duration::from_millis(config.conn_timeout); let ws_heartbeat_timeout = Duration::from_millis(config.heartbeat_timeout); let (tx, rx) = mpsc::unbounded_future::<(usize, Value)>(); try_join!( // Run connection handlers try_join_all((0..par).map(|id| { let tx = tx.clone(); let ws_conn_limiter = ws_conn_limiter.clone(); conn_handler( id, tx, ws_url, ws_conn_limiter, ws_conn_timeout, ws_heartbeat_timeout, ) })), // Run receiver conn_receiver(par, rx, api_url, api_conn_limiter), )?; Ok(()) }
Здесь настраивается пайплайн для получения потока событий из WebSocket API-шки, причем параллельно в несколько коннектов в разные ДЦ (в целях избыточности). События прилетают в виде JSON-строк, которые парсятся параллельно в разных тредах и уже распарсенными передаются в ресивер, который отбрасывает дубли и что-то дальше с ними делает.
easty
21.12.2023 22:54Всё равно как арабская вязь. Набор амперсандов скобок стрелок двоеточий. Можно конечно привыкнуть, но выглядит как вырвиглаз бдсм. Такое ощущение, что разработчики языка хотели создать не сообщество разработчиков, а секту посвящённых.
andreymal
21.12.2023 22:54Вы не первый и не последний, кто пишет такое, и здесь всегда возникает один вопрос — предложите более красивый синтаксис без потери функциональности?
itmind
В реальном коде может возникнуть такое UB?
Пока похоже, что для того, что бы получить это UB нужно специально так написать код. (учитывая, что нужно аргумент функции на placeholder менять)
rukhi7
начать нужно бы было с вопроса:
А есть ли для Rust стандарт?
Потому что UB это то что определено в стандарте.
Пока нет стандарта нет и UB.
MiyuHogosha
Скорее это то, что НЕ определено в стандарте и стандарт не требует документирования.
Таким образом, если пользоваться наивной логикой, весь Раст -UB ( да помилует меня дух Рассела)
rukhi7
Справедливости ради undefined behavior это все таки термин определенный стандартом, также как unspecified behavior, implementation-defined behavior.
или
Но мне нравится ход ваших мыслей.
Похоже надо заняться, перевести как нибудь С-шный стандарт :), выложить на Хабре :) .
Pavel_Agafonov Автор
По второй ссылке представлены другие варианты бага, например, вместо placeholder-а используется Option:
let f: fn(Option<&'d &'d ()>, &'c T) -> &'d T = foo;
(Сигнатура helper-а другая).Возникнуть теоретически может, если писать код с временами жизни и с вложенными ссылками, а не с
Rc<RefCell<T>>
:)