Если вы никогда не сталкивались с 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)


  1. itmind
    21.12.2023 22:54

    В реальном коде может возникнуть такое UB?

    Пока похоже, что для того, что бы получить это UB нужно специально так написать код. (учитывая, что нужно аргумент функции на placeholder менять)


    1. rukhi7
      21.12.2023 22:54

      начать нужно бы было с вопроса:

      А есть ли для Rust стандарт?

      Потому что UB это то что определено в стандарте.

      Пока нет стандарта нет и UB.


      1. MiyuHogosha
        21.12.2023 22:54

        Скорее это то, что НЕ определено в стандарте и стандарт не требует документирования.

        Таким образом, если пользоваться наивной логикой, весь Раст -UB ( да помилует меня дух Рассела)


        1. rukhi7
          21.12.2023 22:54

          Справедливости ради undefined behavior это все таки термин определенный стандартом, также как unspecified behavior, implementation-defined behavior.

          3.4.3 1 undefined behavior behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

          или

          Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose. It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior.

          Но мне нравится ход ваших мыслей.

          Похоже надо заняться, перевести как нибудь С-шный стандарт :), выложить на Хабре :) .


    1. Pavel_Agafonov Автор
      21.12.2023 22:54

      По второй ссылке представлены другие варианты бага, например, вместо placeholder-а используется Option:
      let f: fn(Option<&'d &'d ()>, &'c T) -> &'d T = foo; (Сигнатура helper-а другая).
      Возникнуть теоретически может, если писать код с временами жизни и с вложенными ссылками, а не с Rc<RefCell<T>> :)


  1. Dmitri-D
    21.12.2023 22:54

    Сыроватый язык. Недавно столкнулся с transmute из non-mut в mut cсылку c другим типом работал в rust 1.53 и никто не замечал проблемы, а в 1.7 перестало вообще вызываться - оптимизатор просто выкидывал код. Без оптимизатора - поведение соответствует 1.53. Понятно что так нельзя, но тоже хотелось бы видеть warning, а лучше error.


    1. Starche
      21.12.2023 22:54

      Transmuting an & to &mut is always Undefined Behavior.

      No you can't do it.

      No you're not special.

      https://doc.rust-lang.org/nomicon/transmutes.html


      1. Dmitri-D
        21.12.2023 22:54

        Sorry if you misunderstood my russian. Correct, transmute this way is UB. The point is to have WARNING or ERROR suggesting to fix the issue.


        1. 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 не возникает?


  1. Vlafy2
    21.12.2023 22:54

    Какой мерзкий синтаксис, кто это выдумал? Совсем нечитаемый код.


    1. 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-строк, которые парсятся параллельно в разных тредах и уже распарсенными передаются в ресивер, который отбрасывает дубли и что-то дальше с ними делает.


      1. easty
        21.12.2023 22:54

        Всё равно как арабская вязь. Набор амперсандов скобок стрелок двоеточий. Можно конечно привыкнуть, но выглядит как вырвиглаз бдсм. Такое ощущение, что разработчики языка хотели создать не сообщество разработчиков, а секту посвящённых.


        1. andreymal
          21.12.2023 22:54

          Вы не первый и не последний, кто пишет такое, и здесь всегда возникает один вопрос — предложите более красивый синтаксис без потери функциональности?


        1. sdramare
          21.12.2023 22:54

          Язык С для вас тоже арабская вязь?