Evaluation of constant value failed, out-of-bounds pointer arithmetic

I'm trying to create a constant with an address, but the compiler can't do it:

struct PhysAddr(u64);
struct VirtAddr(u64);
const PHYSICAL_MEMORY_MAPPING_OFFSET: u64 = 0xFFFF_A000_0000_0000;
const LOCAL_APIC_BASE_ADDR: PhysAddr = PhysAddr(0xFEE00000);
const LOCAL_APIC_BASE_MAPPED_ADDR: VirtAddr = VirtAddr(LOCAL_APIC_BASE_ADDR.0 + PHYSICAL_MEMORY_MAPPING_OFFSET);
const SPURIOUS_INTERRUPT_VECTOR_REGISTER: *mut u32 = unsafe { LOCAL_APIC_BASE_MAPPED_ADDR.as_mut_ptr::<u32>().byte_add(0xF0) };

impl VirtAddr {
    pub const fn as_mut_ptr<T>(&self) -> *mut T {
        self.0 as *mut T
    }
}

fn main() {}

(Rust Playground)

I get the error “out-of-bounds pointer arithmetic: expected a pointer to 240 bytes of memory, but got 0xffffffa000fee00000[noalloc] which is a dangling pointer (it has no provenance)”, but the address is completely valid and not out of 64-bit space.

See How does `integer-pointer` work in the embedded platform?

The short answer is that Rust is moving away from providing ways to access MMIO outside of its abstract machine.

One workaround is doing integer arithmetic instead of pointer arithmetic. That is the same as doing the arithmetic in another language through FFI, for all Rust is concerned. Once you have created a pointer, it will begin tracking its provenance.

Another consideration is to first create the pointer to a slice or struct and do pointer arithmetic so you are always pointing at in-bounds valid allocated objects. (Take this with a grain of salt, the compiler may end up being very unhappy with you creating pointers to objects out of thin air. It ought not be a problem as long as MMIO does not overlap with any address space that Rust considers part on its abstract machine. But I’m not writing the rules, only interpreting them.)

edit: In my opinion, it is better to only use raw pointers to primitive types when doing MMIO access. Because they are all Copy types that cannot violate ownership rules by writing through the pointers! Using higher level types gets ugly very quickly, and that's what usually ends up ruffling feathers when it comes to int-to-ptr and provenance discussions (from my perspective, anyway).

Ok, I just counted in u64 first and then converted to pointer, lol

Thanks for answer.
I partly understand that, but it seems odd anyway, especially since I'm using unsafe and taking responsibility.
The fact that instead of using byte_add() I have to use numeric arithmetic looks like nothing but a fight with the compiler, because the result is the same, I just add bytes to an address that is just a number, the memory location number, everywhere.

It actually is not the same due to subtle reasons. The pointer you created has a spacial region of validity. Advancing the pointer beyond its valid region is something the compiler is always on the lookout for, among many other common mistakes that people have been consistently making for 60 years.

What is this "spacial region of validity"? Is it 4 bytes that u32 takes up?

Specifically, yes it is 4 bytes. Because that's what you told it:

Then what is the byte_add() function for and does it generally work or does it always create a compilation error?

In general, as far as I understand, there is always some “allocated object” behind the pointer and if I understand correctly, the default address arithmetic is wrong from the language point of view, since we get away from this “allocated object”, right?

It works, but only for pointers with provenance. Which are pointers created from live allocated objects. Some examples include:

  • Coerced from &T or &mut T
  • Box::into_raw(), slice::as_ptr()
  • ptr::addr_of!()
  • Calling extern fn()

Yes, that's roughly correct. See: std::ptr - Rust

Specifically, the "Strict Provence" section.

In this case, when I have an address as a number, what is the correct way to create a pointer?

Unfortunately, you are doing all of your pointer arithmetic in const fn(), which excludes the normal workarounds (extern fn() and asm!()).

If you remove that constraint, then something like this could work in a pinch:

impl VirtAddr {
    pub unsafe fn as_mut_ptr<T>(&self) -> *mut T {
        #[allow(unused_assignments)]
        let mut ptr = core::ptr::null_mut();
        core::arch::asm!("mov {}, {}", out(reg) ptr, in(reg) self.0);
        ptr
    }
}

(edit: This is still using the bad "offset pointer beyond u32" code. It should be rejected with the same error.)

It generates unoptimal code (useless copying between GPRs) because the assembly is opaque to Rust. But that's also the reason it's accepted by the compiler. It's basically a poor opaque transmute.

Hmmm, in fact, the original code is accepted as long as it isn't run in const context. I don't know the reason for that. Maybe the provenance checks use const assertions?

I have no idea. It's weird to me anyway.

Ok, I dug around in the rustc and core code a bit, and the reason it fails to error when outside of const context is because there is no guarantee that let bindings are const evaluated. Even in cases where it can trivially be done (all function calls are const and all arguments are constants).

That satisfies my curiosity. I think this limitation with let may be lifted in the future.

Still, I have no conclusion for how to do this "correctly". It needs compiler support in the form of [strict provenance] Provide a way to "create allocations at a fixed address" · Issue #98593 · rust-lang/rust. The workarounds using side effects are not eligible for const context evaluation.

1 Like

It is still detected as UB by Miri, even if compiler fails to see it.

(I'm not able to create playground link using the phone for some reason)

2 Likes