Есть ли 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<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-блоке для времен жизни.

Ссылки


ссылка на оригинал статьи https://habr.com/ru/articles/782412/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *