Есть ли Undefined Behavior в Rust?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

Ссылки

  • fake-static крейт

  • Implied bounds on nested references + variance = soundness hole

  • Counterexamples in Type Systems

  • Tracking Issue for -Znext-solver 

  • Officially announcing the types team

  • RFC Well formed and variance

Источник: https://habr.com/ru/articles/782412/


Интересные статьи

Интересные статьи

В моей книге “Просто о мозге” была ещё одна глава, которую я удалил перед публикацией. Она называлась “Будущее”. В ней я приводил прогнозы развития человечества на основе того, что сейчас известно о м...
В современном С++ осталось не так много вещей, которые не подходят под парадигму "Не плати за то, что не используешь". Одна из них – dynamic_cast. В рамках данной статьи мы разберёмся, что с ним не та...
Привет! Я Виктор Барсуков, Java-разработчик в Lamoda. В этой статье хочу рассказать о своем опыте разработки десктопного Java-приложения в рамках пет-проджекта. Что из этого получилось и что можно был...
Давняя проблема стриминговых сервисов связана с тем, что до музыкантов доходит менее цента за одно прослушивание трека. Чтобы заработать больше, некоторые исполнители обращаются к альтернативным спосо...
В комментариях к прошлой статье о low-code в enterprise-решениях я увидел резонное количество типичных возражений по LCDP. Этим постом я постараюсь ответить на них. Разбе...