Вектор пуст после клонирования структуры с неинициализированным членом - PullRequest
0 голосов
/ 15 сентября 2018

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

Ответы [ 2 ]

0 голосов
/ 16 сентября 2018

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


Запуск вашего кода в 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::<[closure@DefId(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::<[closure@DefId(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, [closure@DefId(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::<[closure@DefId(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-образному, является гигантским поножом и часто используется неправильно (удивительно!), так что вы нездесь одинВ качестве замены вы должны следовать следующим указаниям:

0 голосов
/ 16 сентября 2018

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

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

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

francis@francis-arch /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.

оптимизирован.
println!("{}", std::mem::size_of::<Option<std::mem::ManuallyDrop<CountDrop<'static>>>>());
// prints 16 in Rust 1.28, but 8 in Rust 1.29

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

...