Вектор пуст после клонирования структуры с неинициализированным членом

В Rust 1.29.0 один из моих тестов начал сбой. Мне удалось получить странный баг к этому примеру:

#[derive(Clone, Debug)]
struct CountDrop<'a>(&'a std::cell::RefCell<usize>);

struct MayContainValue<T> {
    value: std::mem::ManuallyDrop<T>,
    has_value: u32,
}

impl<T: Clone> Clone for MayContainValue<T> {
    fn clone(&self) -> Self {
        Self {
            value: if self.has_value > 0 {
                self.value.clone()
            } else {
                unsafe { std::mem::uninitialized() }
            },
            has_value: self.has_value,
        }
    }
}

impl<T> Drop for MayContainValue<T> {
    fn drop(&mut self) {
        if self.has_value > 0 {
            unsafe {
                std::mem::ManuallyDrop::drop(&mut self.value);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn check_drops() {
        let n = 2000;
        let drops = std::cell::RefCell::new(0usize);

        let mut slots = Vec::new();
        for _ in 0..n {
            slots.push(MayContainValue {
                value: std::mem::ManuallyDrop::new(CountDrop(&drops)),
                has_value: 1,
            });
        }

        unsafe { std::mem::ManuallyDrop::drop(&mut slots[0].value); }
        slots[0].has_value = 0;

        assert_eq!(slots.len(), slots.clone().len());
    }
}

Я знаю, что код выглядит странно; это все вырвано из контекста. Я воспроизвел эту проблему с cargo test на 64-битном Ubuntu на Rust 1.29.0. Друг не смог воспроизвести на Windows с той же версией Rust.

Другие вещи, которые останавливают воспроизведение:

  • Понижение n ниже ~ 900.
  • Не выполняйте пример из cargo test.
  • Замена CountDrop элемента с u64.
  • Использование версии Rust до 1.29.0.

Что здесь происходит? Да, MayContainValue может иметь неинициализированный элемент, но это никогда не используется.

Мне также удалось воспроизвести это на play.rust-lang.org.


Меня не интересуют "решения", которые связаны с реорганизацией MayContainValue безопасным способом с помощью Option или enum, я использую ручное хранилище и занятую/незанятую дискриминацию по уважительной причине.

Ответы

Ответ 1

TL; DR: Да, создание неинициализированной ссылки - это всегда неопределенное поведение. Вы не можете использовать mem::uninitialized безопасно с generics. В настоящее время нет хорошего пути для вашего конкретного случая.


Запуск кода в valgrind сообщает о 3 ошибках, каждая из которых имеет одну и ту же трассировку стека:

==741== Conditional jump or move depends on uninitialised value(s)
==741==    at 0x11907F: <alloc::vec::Vec<T> as alloc::vec::SpecExtend<T, I>>::spec_extend (vec.rs:1892)
==741==    by 0x11861C: <alloc::vec::Vec<T> as alloc::vec::SpecExtend<&'a T, I>>::spec_extend (vec.rs:1942)
==741==    by 0x11895C: <alloc::vec::Vec<T>>::extend_from_slice (vec.rs:1396)
==741==    by 0x11C1A2: alloc::slice::hack::to_vec (slice.rs:168)
==741==    by 0x11C643: alloc::slice::<impl [T]>::to_vec (slice.rs:369)
==741==    by 0x118C1E: <alloc::vec::Vec<T> as core::clone::Clone>::clone (vec.rs:1676)
==741==    by 0x11AF89: md::tests::check_drops (main.rs:51)
==741==    by 0x119D39: md::__test::TESTS::{{closure}} (main.rs:36)
==741==    by 0x11935D: core::ops::function::FnOnce::call_once (function.rs:223)
==741==    by 0x11F09E: {{closure}} (lib.rs:1451)
==741==    by 0x11F09E: call_once<closure,()> (function.rs:223)
==741==    by 0x11F09E: <F as alloc::boxed::FnBox<A>>::call_box (boxed.rs:642)
==741==    by 0x17B469: __rust_maybe_catch_panic (lib.rs:105)
==741==    by 0x14044F: try<(),std::panic::AssertUnwindSafe<alloc::boxed::Box<FnBox<()>>>> (panicking.rs:289)
==741==    by 0x14044F: catch_unwind<std::panic::AssertUnwindSafe<alloc::boxed::Box<FnBox<()>>>,()> (panic.rs:392)
==741==    by 0x14044F: {{closure}} (lib.rs:1406)
==741==    by 0x14044F: std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:136)

Уменьшение при сохранении ошибки Valgrind (или одного очень похожего) приводит к

use std::{iter, mem};

fn main() {
    let a = unsafe { mem::uninitialized::<&()>() };
    let mut b = iter::once(a);
    let c = b.next();
    let _d = match c {
        Some(_) => 1,
        None => 2,
    };
}

Выполнение этого меньшего воспроизведения в Miri на игровой площадке приводит к этой ошибке:

error[E0080]: constant evaluation error: attempted to read undefined bytes
 --> src/main.rs:7:20
  |
7 |     let _d = match c {
  |                    ^ attempted to read undefined bytes
  |
note: inside call to 'main'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:74:34
  |
74|     lang_start_internal(&move || main().report(), argc, argv)
  |                                  ^^^^^^
note: inside call to 'closure'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:59:75
  |
59|             ::sys_common::backtrace::__rust_begin_short_backtrace(move || main())
  |                                                                           ^^^^^^
note: inside call to 'closure'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/sys_common/backtrace.rs:136:5
  |
13|     f()
  |     ^^^
note: inside call to 'std::sys_common::backtrace::__rust_begin_short_backtrace::<[[email protected](1/1:1823 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:59:13
  |
59|             ::sys_common::backtrace::__rust_begin_short_backtrace(move || main())
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside call to 'closure'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:310:40
  |
31|             ptr::write(&mut (*data).r, f());
  |                                        ^^^
note: inside call to 'std::panicking::try::do_call::<[[email protected](1/1:1822 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:306:5
  |
30| /     fn do_call<F: FnOnce() -> R, R>(data: *mut u8) {
30| |         unsafe {
30| |             let data = data as *mut Data<F, R>;
30| |             let f = ptr::read(&mut (*data).f);
31| |             ptr::write(&mut (*data).r, f());
31| |         }
31| |     }
  | |_____^
note: inside call to 'std::panicking::try::<i32, [[email protected](1/1:1822 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe]>'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:392:9
  |
39|         panicking::try(f)
  |         ^^^^^^^^^^^^^^^^^
note: inside call to 'std::panic::catch_unwind::<[[email protected](1/1:1822 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:58:25
  |
58|           let exit_code = panic::catch_unwind(|| {
  |  _________________________^
59| |             ::sys_common::backtrace::__rust_begin_short_backtrace(move || main())
60| |         });
  | |__________^
note: inside call to 'std::rt::lang_start_internal'
 --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:74:5
  |
74|     lang_start_internal(&move || main().report(), argc, argv)
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Короткий вариант: mem::uninitialized создает нулевой указатель, который рассматривается как ссылка. Это неопределенное поведение.

В исходном коде Vec::clone реализуется путем итерации по итератору. Iterator::next возвращает параметр Option<T>, поэтому у вас есть опция ссылки, которая заставляет нулевую оптимизацию указателя входить. Это считается как None, что рано заканчивает итерацию, в результате чего ваш пустой второй вектор.

Оказывается, имея mem::uninitialized, фрагмент кода, который дает вам C-подобную семантику, представляет собой гигантский фуганок и часто используется неправильно (неожиданно!), Поэтому вы здесь не одиноки. Главные вещи, которые вы должны соблюдать в качестве замены:

Ответ 2

Rust 1.29.0 изменил определение ManuallyDrop. Это был union (с одним членом), но теперь это struct и элемент lang. Роль элемента lang в компиляторе заключается в том, чтобы заставить тип не иметь деструктор, даже если он обертывает тип, который имеет один раз.

Я попытался скопировать старое определение ManuallyDrop (которое требует ночной, если не добавлено ограничение T: Copy) и использование этого вместо него из std, и это позволяет избежать проблемы (по крайней мере, на игровой площадке). Я также попытался сбросить второй слот (slots[1]) вместо первого (slots[0]), и это также срабатывает.

Хотя я не смог воспроизвести проблему изначально в своей системе (работает Arch Linux x86_64), я нашел что-то интересное, используя miri:

[email protected] /data/git/miri master
$ MIRI_SYSROOT=~/.xargo/HOST cargo run -- /data/src/rust/so-manually-drop-1_29/src/main.rs
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s                                                                                                                                                                                
     Running 'target/debug/miri /data/src/rust/so-manually-drop-1_29/src/main.rs'
error[E0080]: constant evaluation error: attempted to read undefined bytes
    --> /home/francis/.rustup/toolchains/nightly-2018-09-15-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/vec.rs:1903:32
     |
1903 |                 for element in iterator {
     |                                ^^^^^^^^ attempted to read undefined bytes
     |
note: inside call to '<std::vec::Vec<T> as std::vec::SpecExtend<T, I>><MayContainValue<CountDrop>, std::iter::Cloned<std::slice::Iter<MayContainValue<CountDrop>>>>::spec_extend'
    --> /home/francis/.rustup/toolchains/nightly-2018-09-15-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/vec.rs:1953:9
     |
1953 |         self.spec_extend(iterator.cloned())
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside call to '<std::vec::Vec<T> as std::vec::SpecExtend<&'a T, I>><MayContainValue<CountDrop>, std::slice::Iter<MayContainValue<CountDrop>>>::spec_extend'
    --> /home/francis/.rustup/toolchains/nightly-2018-09-15-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/vec.rs:1402:9
     |
1402 |         self.spec_extend(other.iter())
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside call to '<std::vec::Vec<T>><MayContainValue<CountDrop>>::extend_from_slice'
    --> /home/francis/.rustup/toolchains/nightly-2018-09-15-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/slice.rs:168:9
     |
168  |         vector.extend_from_slice(s);
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside call to 'std::slice::hack::to_vec::<MayContainValue<CountDrop>>'
    --> /home/francis/.rustup/toolchains/nightly-2018-09-15-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/slice.rs:369:9
     |
369  |         hack::to_vec(self)
     |         ^^^^^^^^^^^^^^^^^^
note: inside call to 'std::slice::<impl [T]><MayContainValue<CountDrop>>::to_vec'
    --> /home/francis/.rustup/toolchains/nightly-2018-09-15-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/vec.rs:1687:9
     |
1687 |         <[T]>::to_vec(&**self)
     |         ^^^^^^^^^^^^^^^^^^^^^^
note: inside call to '<std::vec::Vec<T> as std::clone::Clone><MayContainValue<CountDrop>>::clone'
    --> /data/src/rust/so-manually-drop-1_29/src/main.rs:54:33
     |
54   |         assert_eq!(slots.len(), slots.clone().len());
     |                                 ^^^^^^^^^^^^^
note: inside call to 'tests::check_drops'
    --> /data/src/rust/so-manually-drop-1_29/src/main.rs:33:5
     |
33   |     tests::check_drops();
     |     ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try 'rustc --explain E0080'.

(Примечание. Я могу получить ту же ошибку без использования Xargo, но затем miri не показывает исходный код для стековых фреймов в std.)

Если я снова это сделаю с оригинальным определением ManuallyDrop, то miri не сообщит о какой-либо проблеме. Это подтверждает, что новое определение ManuallyDrop заставляет вашу программу иметь неопределенное поведение.

Когда я меняю std::mem::uninitialized() на std::mem::zeroed(), я могу достоверно воспроизвести проблему. При запуске изначально, если произойдет, что неинициализированная память - все нули, тогда вы получите эту проблему, иначе вы не будете.

Вызывая std::mem::zeroed(), я заставил программу генерировать нулевые ссылки, которые описаны как неопределенное поведение в Rust. Когда вектор клонирован, используется итератор (как показано в выводе miri выше). Iterator::next возвращает параметр Option<T>; что T здесь имеет ссылку в нем (поступает из CountDrops), что заставляет оптимизировать макет Option памяти: вместо дискретного дискриминанта он использует нулевую ссылку для представления ее значения None. Поскольку я генерирую нулевые ссылки, итератор возвращает None в первом элементе, и, следовательно, вектор заканчивается пустым.

Интересно, что когда ManuallyDrop был определен как объединение, макет Option memory не был оптимизирован.

println!("{}", std::mem::size_of::<Option<std::mem::ManuallyDrop<CountDrop<'static>>>>());
// prints 16 in Rust 1.28, but 8 in Rust 1.29

Об этой ситуации обсуждается в # 52898.