Is this a rustc bug related with dead store elimination?

the function change_value() has UB because you are creating an invalid &mut Data from a raw pointer derived from &Data:

fn change_value(addr: usize, value: i32) {
    unsafe {
        let d = &mut *(addr as *mut Data);
        d.value = value;
    }
}

pub fn main() {
    let data = Data { value: 42 };
    let addr = &data as *const Data as usize;
    change_value(addr, 0xffff);
    //...
}

EDIT:

you have also done a pointer-to-integer-to-pointer conversion, which lost the provenance of the pointer.

3 Likes

And, the call to foreign_function() does not appear in the assembly because it can never happen before UB happens.

Try writing a version of your program that is accepted by Miri (you can run Miri from the “Add tool” menu in Compiler Explorer), before supposing a compiler bug.

2 Likes

But is it common to convert a usize address into a &mut reference in FFI?
That is, Rust creates a struct, passes its address to foreign functions, and those foreign functions may later pass the address back and ask Rust to mutate the original struct.

What is the best practice for handling this pattern?
How can I ensure that the mutation operation is not optimized away by Rust?

Use raw pointers and observe strict provenance to the extent possible.

Read these docs:

Are you suggesting changing the code like this? Maybe I was mistaken in thinking that, for FFI functions, foo(addr: usize) is the same as foo(addr: *mut Data).

I should clarify a bit. the pointer-integer-pointer roundtrip is NOT a problem by itself, however it does opt-out strict provenance, replacing it with exposed provenance, so you need to be extra careful when dealing with such use cases.

the root cause of the UB in the provided snippet is that, the raw pointer is originated from a shared reference &data.

in fact, because there's no UnsafeCell in the definition of Data, and the variable data is not declared mut, changing the value of it in any manner is UB.

stick with pointer types, preferrably safe pointers (references and smart pointers), as much as possible, only convert to raw pointers (or integers) at the ffi callsite, and document the details of safety conditions, make sure you understand the implication of each appearance of the unsafe keyword.

this example is not "optimized" away. the optimizer is not allowed to change the obverved behavior of the program, given the program is UB free. however, if there's UB in the program, all bets are off.

1 Like

Or maybe something like this. But yes, stick to pointers if possible, not integers. The details can depend on the use case, like is the pointer opaque on the FFI side or not, etc.

(Converting from &mut Data instead of from &Data was fixing an UB bug regardless.)

Questionable given provenance. In addition to the RFC and docs I linked before, you could read Ralf's Pointers are Complicated blog posts. But be forewarned that it's a big, evolving topic.

Whenever possible, when you have to leave safe Rust reference land (for FFI or whatever), try to enter raw pointer land and stay there until you're done needing raw pointers. Avoid integer round-trips wherever possible, and if you need a safe reference while there are still raw pointers about, create a transient one from an existing raw pointer derived from your original borrow.

I could try to write more, but I'd more or less just be echoing the docs. I do also recognize that there's a lot to take in, and writing sound unsafe is difficult. It will take some practice. Keep asking questions when in doubt.

Write tests for your code and check your unsafe code with Miri.

I can try to stick to raw pointers, but there are cases where that’s not possible. For example, I may need to access MMIO using a raw address. How can this be done reliably? Does Rust provide any guarantees in this scenario if I convert an MMIO address to a raw pointer?

MMIOs are outside rust's abstract machine, you cannot use normal pointer dereference operation to access such addresses, you must use the volatile read/write primitives, in which case, there's no provenance associated with the pointer.

in such cases, you can obtain the pointer using std::ptr::without_provenance(). see details in the documentations:

1 Like

If vring_avail_addr points to non-Rust memory, is the following code incorrect because it first casts a usize to a raw pointer, and then converts that raw pointer directly into a Rust reference?

let vring_avail: &AvailRing<N_Q_NUM> =
            unsafe { &*(vring_avail_addr as *const AvailRing<N_Q_NUM>) };
let idx = vring_avail.ring[self.last_avail_idx[q] as usize % N_Q_NUM] as usize;

Should it be written instead as shown below, using core::ptr::without_provenance and working entirely with raw pointers?

let vring_avail =
    core::ptr::without_provenance::<*const AvailRing<N_Q_NUM>>(vring_avail_addr);

let idx_ptr = unsafe {
    core::ptr::addr_of!((*vring_avail).ring)
        .cast::<u16>()
        .add(self.last_avail_idx[q] as usize % N_Q_NUM)
};
let idx = unsafe { core::ptr::read_volatile(idx_ptr) } as usize;

I believe it's technically incorrect.

however, this kind of code is used in practice anyway, especially for the embedded targets. for instance, vcell is a dependency of many projects (it has 785 dependents on crates.io as of now), despite it is known to be problematic:

technically, a raw pointer casted from an integer has an exposed provenance, but whether such pointer is dereferenceable is under specified. and even if it is dereferenceable, it is not sufficient for it to be converted to a reference, the conditions are documented here, although some the rules are not completely clear to me.

I would recommend so.

you should only use raw pointers for foreign addresses, including MMIO, and only use read_volatile()/write_volatile() to access these addresses in rust[1].

there have been attempts to use regular references for MMIOs, but as far as I know, (almost?) all of them are unsound. see e.g. this (very long) discussion and related links:

however, I don't think you have to use without_provenance() to create the raw pointers for MMIO, "exposed" provenance should also work: to my understanding, foreign memory addresses are always considered "exposed", as long as they are disjoint from the memory of the rust abstract machine.


  1. or alternatively, access them outside rust, e.g. ffi or inline assembly ↩︎

1 Like

We need to distinguish between things.

This pattern is incorrect, but provenance-wise is fine. As said here, MMIO is external to the AM and considered exposed. The cast creates a valid pointer.

The problems with references are different and related to unique invariants of references. And AFAIK, only creating &UnsafeCell references to MMIO memory is sound, but it's not useful, because it's not guaranteed the compiler won't omit read/writes or perform spurious read/writes.

1 Like