Звук `&T as *const T as *mut T` для элементов `static mut`?

Представьте, что вы пытаетесь написать безопасную оболочку для некоторой изменяемой статики:

#![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 и писать в полученный необработанный указатель, но ни строгий провенанс, ни Мири не жалуются на приведенный выше код. Является ли это ложным отрицательным результатом из-за ограничений сложенных заимствований и/или Мири? Или static mut предметы каким-то образом помечаются как SharedRW глобально и получают здесь бесплатный проход? Ctrl-F для «статического» в статье Stacked Borrows не выявил ничего существенного.

Я также думал о чем-то подобном, что я бы предпочел, потому что это заставляет T пережить Wrapper.

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);
// ...

но это явно несостоятельно (о чем вам скажет Мири), потому что для этого требуется, чтобы &GLOBAL_SINGLETON был жив, пока &UnsafeCell { GLOBAL_SINGLETON } жив.

Просто обратите внимание, что модель Stacked Borrows может быть заменена — акцент на мае — моделью Tree Borrows, которая имеет немного другие правила.

Matthieu M. 20.06.2023 16:26

@MatthieuM. Заметьте, что Stacked Borrows не является (пока?) официальной моделью памяти Rust, и выбранная модель памяти может не быть ни Stacked Borrows, ни Tree Borrows.

Chayim Friedman 20.06.2023 16:28

Также может быть интересно посмотреть на feature(const_mut_refs), с которой можно сделать &mut GLOBAL_SINGLETON as *mut _ напрямую. Это может указывать на то, как static mut обрабатывается внутри.

Matthieu M. 20.06.2023 16:29

@ChayimFriedman Это хороший момент. Текущая модель памяти более или менее полностью не указана, верно? За исключением, может быть, семантики общих и изменяемых ссылок?

isaactfa 20.06.2023 16:55

@MatthieuM. Спасибо за подсказку, я не знал об этой функции. Я просмотрел древовидные заимствования, когда они появились в последнем выпуске TWIR, но, учитывая, что многоуровневые заимствования даже не являются официальной моделью памяти Rust, я особо не вникал в это. Я просто решил следовать многоуровневым заимствованиям, потому что это то, что реализует Мири (я полагаю).

isaactfa 20.06.2023 16:57

Я бы поспорил, что это должно быть классифицировано как неопределенное поведение, тем более что текущая (разреженная) документация очень непреклонна в том, что & to &mut не разрешено и не квалифицирует это дальше с классом хранения, и поскольку &mut GLOBAL_SINGLETON было бы возможно в этом случае в будущее (комментарий Матье). Я бы расценил это как ложноотрицательный результат от miri, который сам говорит, что не улавливает все случаи неопределенного поведения.

kmdreko 20.06.2023 17:07

@isaactfa: Мири на самом деле реализует и то, и другое. Stacked Borrows используется по умолчанию, и вы можете активировать Tree Borrows, передав -Zmiri-tree-borrows (который передается переменной среды...). Ни один из них не является официальным в основном потому, что они являются рабочими инструментами, используемыми для изучения предметной области и влияния выбора.

Matthieu M. 20.06.2023 17:07
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
7
95
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

  1. Это немедленное неопределенное поведение, потому что вы не можете сделать &mut T из &T каким-либо образом, за исключением случаев, когда T является UnsafeCell. Вы должны написать что-то вроде этого, по крайней мере:
Wrapper(unsafe { &mut GLOBAL_SINGLETON as *mut _ })

MIRI не улавливает этот конкретный пример, но если вы запустите более простую программу, вы увидите, что:

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

Выход МИРИ:

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 о преобразовании общих ссылок в изменяемые ссылки.

  1. API Wrapper и Guard, хотя и не вызывают неопределенного поведения сами по себе, все же ненадежны. Это означает, что вы можете запускать UB из безопасного Rust, используя их.

Например, это будет 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);

Я считаю, что вы правы, но у вас есть доказательства? Ведь Мири об этом не сообщает. Не то, чтобы он сообщал обо всех Неопределенных Поведениях, но я чувствую, что об этом нужно было сообщать, если UB.

Chayim Friedman 20.06.2023 18:13

@ChayimFriedman См. запись о преобразовании ссылок: doc.rust-lang.org/nomicon/transmutes.html Приведение ссылки к указателю и разыменование указателя по существу такое же, как преобразование, с той лишь разницей, что преобразование более удобно для оптимизатора LLVM.

Angelicos Phosphoros 20.06.2023 18:21

@ChayimFriedman Если вы используете более простую программу, MIRI пометит такой состав: play.integer32.com/…

Angelicos Phosphoros 20.06.2023 18:23

Это не использование static mut, это совсем другое дело.

Chayim Friedman 20.06.2023 19:56

Это то же самое для правильности, единственное отличие состоит в сложности анализа компилятором, поскольку глобальную память нельзя отслеживать. Это одна из причин, по которой доступ к static mut небезопасен: программа проверки заимствований не может реально отследить и убедиться, что все в порядке.

Angelicos Phosphoros 20.06.2023 20:16

Мы говорим не о программе проверки заимствований, а о Miri, которая является динамическим анализатором и должна быть в состоянии сделать это довольно легко.

Chayim Friedman 20.06.2023 20:17

Спасибо за подробный ответ! Эти комментарии и ответы заставили меня понять, что мой вопрос на самом деле не о алиасинге (правила которого, я думаю, я хорошо понимаю), а о том, как статическая оценка работает в Rust; т. е. эти отливки могут быть оптимизированы до того, как будет применена проверка заимствования/происхождения.

isaactfa 20.06.2023 23:36
Ответ принят как подходящий

В обоих существующих ответах указано, что это UB, поэтому я добавляю один из своих, чтобы отразить более тонкий вклад Ральфа Юнга. Если кто и знает, я думаю, это он:

Это связано с тем, что у нас нет модели псевдонимов во время компиляции, поэтому ничто не помнит, что этот указатель происходит из общей ссылки. Мы также не приняли решения о том, применяются ли правила псевдонимов даже на границе между временем компиляции и временем выполнения. Я думаю, что они здесь не очень полезны, поскольку все глобально... Но это вопрос UCG. 1

и

Это глубоко на неопределенной/неопределенной территории. Так что, пожалуйста, не делай этого. :) 2

Так что это не определенно UB, но это также не совсем UB, и его следует избегать до принятия решения.

Ральф также создал новую проблему в репозитории UCG, чтобы решить эту проблему, так что следите за ней в будущем.

1: https://github.com/rust-lang/miri/issues/2937#issuecomment-1600365067

2: https://github.com/rust-lang/miri/issues/2937#issuecomment-1602458297

Другие вопросы по теме

Законен ли доступ к массивам за пределами границ, если то, что находится за этими пределами, известно в C? Почему бы и нет, и как это можно обойти?
Пример неопределенного поведения с использованием const_cast
Неожиданный memcpy для некопируемого и неперемещаемого типа при использовании co_await
Почему значение мусора не возвращается при рекурсивном вызове функции с неопределенным поведением?
Построение объекта над самим собой
Токенизатор в C не показывает никаких выходных данных
Должны ли lvalue типа T идентифицировать объекты типа T? Если `p` имеет тип `T *`, требуется ли `&*p`, чтобы `p` фактически указывал на объект типа `T`?
Как приведение и вызов obj_msgSend() не вызывает неопределенное поведение?
Какая формулировка делает этот вызывающий двойное освобождение вызов `std::vector<T>::clear()` в неопределенном поведении конструктора `T`?
Различное поведение при отладке и выпуске