Linker scripts, addr_of and undefined behaviour

A common pattern used in the embedded world works as follows:

First you define an area in the linker script, and expose its boundaries through symbols (keep in mind that in this specific example the area has a fixed size, but this is not always the case, and more often then note the actual size depends on the compilation artifact itself)

/* ... */
.heap (NOLOAD)
{
    __heap_start = .;
    . += 4M;  
    __heap_end = .;
} > SRAM
/* ... */

Then in the application code you access that area in this way:

unsafe extern "C" {
    unsafe static mut __heap_start: u8;
    unsafe static mut __heap_end: u8;
}

unsafe {
    allocator::init(addr_of_mut!(__heap_start), addr_of_mut!(__heap_end));
}

My question is, aren't all accesses through addr_of_mut!(__heap_start) and addr_of_mut!(__heap_end) UB?

The main reason for that any access outside that u8 is technically outside its allocation because Rust doesn't now about our linker script shenanigans.

For example, the cortex-m crate does exactly this.

If this is UB, what would be the correct way of doing this? Is casting the pointer to usize and then back to pointer through with_exposed_provenance a better approach? Since it deletes provenance and then gives the compiler a chance to assign a better one? Or is this something we just have to deal with and the compiler is somehow guaranteed to always behave in these scenarios?

See the discussion at

Adding two cents but don't know in any certainty the answer. (Baring above issue saying it's OK.)
Would using c_void be better as it isn't considered an allocation unit, like the u8 could be considered.

c_void is implemented as wrapper around u8.

this conv seems to imly that it should use () or another ZST type,
as __heap_end is not referencable for 1 bytes, but the AM might assume it is if it is declared as u8