rust `&T as *const T as *mut T`对于`static mut`项是否是声音?

zzwlnbp8  于 2023-06-23  发布在  其他
关注(0)|答案(2)|浏览(148)

想象一下,你正试图为一些可变的静态对象编写一个安全的 Package 器:

#![feature(strict_provenance)]
#![deny(fuzzy_provenance_casts)]
#![deny(lossy_provenance_casts)]

struct Wrapper<T>(*mut T);
struct Guard<T>(*mut T);

impl<T> Wrapper<T> {
    fn guard(&self) -> Guard<T> {
        Guard(self.0)
    }
}

// imagine there's synchronization going on that makes this all safe
unsafe impl<T: Send + Sync> Sync for Wrapper<T> {}
impl<T> std::ops::Deref for Guard<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        unsafe { &*self.0 }
    }
}
impl<T> std::ops::DerefMut for Guard<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { &mut *self.0 }
    }
}

static mut GLOBAL_SINGLETON: isize = 0; // Imagine this couldn't be trivially replaced by an atomic type
static WRAPPER: Wrapper<isize> = Wrapper(unsafe { &GLOBAL_SINGLETON as *const _ as *mut _ });
//                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                             Is this sound in this sepcific instance?

// Actually dereference the raw pointer to provoke UB
fn main() {
    assert_eq!(0, *WRAPPER.guard());
    *WRAPPER.guard() += 1;
    assert_eq!(1, *WRAPPER.guard());
}

现在,通常情况下,强制转换&T as *const T as *mut T并写入结果原始指针是不合理的,但严格出处和Miri都没有抱怨上述代码。这是否是由于堆叠借用和/或Miri的限制而导致的假阴性?或者static mut项目以某种方式在全球范围内被标记为SharedRW,并在这里获得免费通行证?在Stacked Borrows paper中使用Ctrl-F表示“静态”并没有显示任何相关内容。
我也考虑过类似的东西,我更喜欢它,因为它迫使TWrapper更长寿。

struct Wrapper<'a, T>(&'a std::cell::UnsafeCell<T>);
impl<'a, T> Wrapper<'a, T> {
    const fn new(t: &'a T) -> Self {
        Self(unsafe { std::mem::transmute(t) }
    }
}
// ...

static mut GLOBAL_SINGLETON: isize = 0;
static WRAPPER: Wrapper<'_, isize> = Wrapper::new(&GLOBAL_SINGLETON);
// ...

但这显然是不合理的(Miri会告诉你),因为它要求&GLOBAL_SINGLETON&UnsafeCell { GLOBAL_SINGLETON }活着的时候也活着。

ggazkfy8

ggazkfy81#

1.这是立即未定义的行为,因为除了当T是UnsafeCell时,您无法以任何方式从&T生成&mut T。你至少应该写这样的东西:

Wrapper(unsafe { &mut GLOBAL_SINGLETON as *mut _ })

MIRI没有捕捉到这个特定的例子,但如果你运行更简单的程序,你可以看到:https://play.integer32.com/?version=stable&mode=debug&edition=2021&gist=b881d107244c95bd37d8fb3221a2b267

fn main(){
    let mut val = 5;
    let ptr = &val as *const i32;
    unsafe{
        *(ptr as *mut _) = 7;
    }
}

MIRI输出:

error: Undefined Behavior: attempting a write access using <2665> at alloc1410[0x0], but that tag only grants SharedReadOnly permission for this location
 --> src/main.rs:6:9
  |
6 |         *(ptr as *mut _) = 7;
  |         ^^^^^^^^^^^^^^^^^^^^
  |         |
  |         attempting a write access using <2665> at alloc1410[0x0], but that tag only grants SharedReadOnly permission for this location
  |         this error occurs as part of an access at alloc1410[0x0..0x4]

另请参阅nomicon entry,了解如何将共享引用转换为可变引用。

  1. WrapperGuard的API虽然本身不会导致未定义的行为,但仍然是不健全。这意味着您可以通过使用它们从安全Rust触发UB。
    例如,这将是UB:
fn foo(){
  let mut guard1 = WRAPPER.guard();
  *guard1 = 7;
  let mut guard2 = WRAPPER.guard();
  *guard2 = 5;
  // This access is UB
  assert_eq!(*guard1, 5);
}

如果你调用T的任何方法,它都很容易得到,因为它很难跟踪可重入性。如果你把多线程添加到混合中,情况就更糟了。
我建议不要使用可变的静态数据,因为无论如何都很不方便正确地访问它们。
我可以想象只有这个声音API可变静态:

use std::sync::atomic::{AtomicUsize, Ordering};

pub fn access_mutable_static(
    // This `for <'a>` ensures that caller cannot store reference
    // to static anywhere.
    access_fn: impl for<'a> FnOnce(&'a mut i32),
) {
    static CURRENT_ACCESS_COUNT: AtomicUsize = AtomicUsize::new(0);
    static mut VALUE: i32 = 0;

    if CURRENT_ACCESS_COUNT.fetch_add(1, Ordering::Acquire) != 0 {
        // Relaxed here is OK because we haven't accessed `VALUE` yet.
        CURRENT_ACCESS_COUNT.fetch_sub(1, Ordering::Relaxed);
        panic!("Duplicate access to mutable static!");
    }

    // Use RAII because `access_fn` can panic.
    struct ReleaseAccessCount;
    impl Drop for ReleaseAccessCount {
        fn drop(&mut self) {
            CURRENT_ACCESS_COUNT.fetch_sub(1, Ordering::Release);
        }
    }

    let _guard = ReleaseAccessCount;
    unsafe {
        // SAFETY: function is safe because if static is being already accessed,
        // function would panic.
        access_fn(&mut VALUE);
    }
}

// Access like this:
let mut value_of_static: i32 = 0;
access_mutable_static(|s|value_of_static = *s);
let new_value = calc_value(value_of_static);
access_mutable_static(|s|*s = new_value);
hyrbngr7

hyrbngr72#

两个现有的答案都指出这是UB,所以我添加了一个我自己的答案,以反映Ralf Jung更细致的输入。如果有人知道,我想就是他了:
这是因为我们在编译时没有一个别名模型,所以没有任何东西会记住这个指针来自一个共享引用。我们还没有决定别名规则是否适用于编译时和运行时之间的边界。他们在这里不是超级有用,我认为因为一切都是全球性的...这是一个UCG问题。1
和/或
它处于未确定/未确定的领域。所以请不要这么做。:)2
因此,这不是绝对的UB,但它也不是绝对不是UB,应该避免等待决定。
Ralf还在UCG存储库中创建了a new issue来解决这个问题,因此请参阅未来的开发。
1:https://github.com/rust-lang/miri/issues/2937#issuecomment-1600365067
2:https://github.com/rust-lang/miri/issues/2937#issuecomment-1602458297

相关问题