-
Notifications
You must be signed in to change notification settings - Fork 114
Has anyone else been blocked by the null pointer restriction at address 0x0? #908
Description
Hi everyone,
I'm working on an RFC (Pre-RFC discussion here) that addresses the inability to construct references or slices at address 0x0 under the current abstract machine model.
The motivating case is real: a researcher at Universität Stuttgart's Institute of Space Systems had to fall back to inline assembly to read a bootloader image starting at 0x0 on Vorago chips like VA108xx - a rad-hard MCU used in aerospace - because slice::from_raw_parts constructs &[u8], which is instant UB at that address. (Original post, Source code)
I'd like to understand how widespread this problem is. If you've ever been blocked or forced into workarounds (inline assembly, volatile access, skipping bytes, etc.) because your hardware places valid objects, peripherals, or firmware structures at address 0x0, I'd really appreciate hearing about it - even briefly:
- What target / chip?
- What was at 0x0? (ordinary RAM, interrupt vector, device tree, etc.)
- What workaround did you use?
- Did the workaround cause any issues during code review, testing, or certification?
Your experience would directly strengthen the RFC's motivation and help demonstrate that this isn't an isolated edge case.
RFC tl;dr
Literal satellite researcher at Universität Stuttgart has to reference 0x0 to calculate CRC, Rust AM says "you must either use asm/volatile or waste a byte". This is absurd.
Generalised codes here
// This address is forced by the hardware.
// Rust does not get to choose it.
const BLOB_P: usize = 0;
const _: () = assert!(usize::BITS == 16);
#[unsafe(no_mangle)]
extern "C" fn ignite() -> ! {
// BLOB can never be read volatilely;
// There's no available RAM to copy the entire struct.
let mut blob = unsafe { &mut *(BLOB_P as *mut DevTreeBlob) };
// instant UB upon reference construction
let mapping = blob.foo();
blob.bar |= 0b1;
...
}use core::slice::from_raw_parts as mkslice;
// `map` address is reported by the firmware.
// Rust does not get to choose it.
// Caller ensures there's at least one entry
#[unsafe(no_mangle)]
extern "C" fn spark(map: *const RamLayout, len: NonZeroUsize) -> ! {
for entry in unsafe { mkslice(map, len.get()).iter() } {
// instant UB upon calling `from_raw_parts`
// as `from_raw_parts` constructs `&T`
...
}
...
}Phase 1 - Null-free pointer
- makes 0x0 valid for raw pointer, NOT FOR
&T - performance penalty: almost none, even Linux is built with
-fno-delete-null-pointer-checksfor 15+ yrs (CVE-2009-1897), no performance issue reported. OP patchedrustcto benchmark it - known issue:
corerelies on&Tinternally (e.g.core::ptr::replaceuses&mut *dst); Phase 2 can fix it
Phase 2 - Zeroable reference
- adds a "zeroable reference primitive"(say
@T) alongside&T - something like
usize:NonZero<usize>==@T:&T &TorOption<&T>: untouched, forever- fixes issue of Phase 1
Why not library type:
// compiler/rustc_ty_utils/src/layout.rs:410-414
ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) => {
let mut data_ptr = scalar_unit(Pointer(AddressSpace::ZERO));
if !ty.is_raw_ptr() {
data_ptr.valid_range_mut().start = 1;
}// library/core/src/ptr/mod.rs:1546-1569
pub const unsafe fn replace<T>(dst: *mut T, src: T) -> T {
/* ... */
mem::replace(&mut *dst, src)
}
}uhh
Objections:
- "changing
&Twill break everything" -> accepted,&Twill never be changed - "perf cost is significant" -> benchmarked, no counter-benchmark presented
- "existing unsafe code breaks" -> example requested (#83), no response
- "volatile is enough" -> actual satellite code demanded
slice::from_raw_parts - "library type works" ->
Derefcan't be implemented (lang team member ACK, #102);coredepends on&Tinternally (#109) - "
field projectionsolves it" -> depends on two unstabilised RFCs;corestill routes through&T - "extremely rare use case" -> is a literal satellite project
- "not a problem of Rust" -> what
Thank you for your time.