Pre-RFC: Conditionally-supported volatile access to address 0

Presently in rust address 0 is reserved as the null pointer. This is consistent with mandatory optimizations, such as the nullable pointer layout optimization, as well as llvm itself, which treats address zero as something which does not alias any valid pointer to accessible memory. However, embedded use-cases may run into issues with this, in particular, if address zero is reasonably and necessarily used by a program (because it is either a scratch page, or it intersects some data structure, including potentially a part of the stack). I recommend the following be specified for the functions core::ptr::read_volatile and core::ptr::write_volatile:

  • If the pointer argument to either read_volatile or write_volatile is a null pointer, the operation is conditionally-supported, with implementation-defined semantics.

In this case, this would mean that individual implementations of rust would have to document whether its valid to call {read,write}_volatile with a null pointer and, if so, what the semantics are (which would be recommended to result in a {read,write} of the appropriate size to address 0), and prevents issues when it is necessary to access address 0 from rust code. This would not do any of the following:

  • Change the value of a null pointer
  • Allow references or boxes to be null pointers
  • Allow normal dereferences or non-volatile {read,write} operations
  • Require the llvm backend to perform operations which llvm considers to have undefined behaviour
  • Solve issues where an object (in particular, one with automatic lifetime) could, on some targets, be incidentally created at address 0.

However, it will encourage implementors for embedded targets to consider whether it may be meaningful and/or necessary for the program to access address 0.

An alternative approach could be to make the null pointer implementation-defined, however this would have to be done for Rust 2021, as (0usize as *const ()).is_null() and core::mem::transmute<usize,Option<&T>>(0) are both stablized as being true and none respectively.

2 Likes

Having only those two functions (and no others) specified to potentially allow address 0 on an architecture-specific basis seems reasonable.

Sorry; I misread. Skip this post.

I don't see how this one would be true. If you don't know that it's at address zero, how would you know that you need to use the _volatile methods to access that particular object? (Even something like ptr::read_volatile(&foo) is plausibly UB, since you have to use ptr::read_volatile(ptr::raw_const!(foo)) to avoid making an invalid reference.) Feels like if it's "automatic" then it'd get implicitly used with references, and thus couldn't be at address zero.

So while having some way to use address zero seems plausible, I don't think it would ever be used "incidentally". And that's only a potential loss of the type's alignment worth of bytes of address space, so doesn't feel like it's necessary to ever be used anything other than very manually.

Is this even implementable with LLVM? As in, does LLVM provide a way to perform a read/write (volatile or not) at address 0?

2 Likes

clang has to support -fno-delete-null-pointer-checks, so I imagine yes. Though empirical testing suggests it’s implemented as an annotation at function scope, not at individual loads or stores.

 

I said it does not solve such issues, which, at least for my purposes, I can live with (in my case, if it happens I'm about to overflow the stack anyways).

To my knowledge, no, though if it is possible that may be useful. This is why I said its conditionally-supported. If it is possible, then it could be enabled for architectures where it makes sense, rather than leaving it on always, so as to preserve certain optimizations. I could look into it specifically, and respond on the viability of supporting it on rustc's llvm backend at all.

Ah yes, the null_pointer_is_valid attribute. Seems reasonable to ask for that to be supported on the level of individual accesses.

If LLVM doesn't support it, it is supported for no architecture with that backend -- a different codegen backend would be needed. I don't think there is any chance for the near future that we will accept a feature that cannot be implemented with LLVM.

So, finding a way to do this with LLVM is a prerequisite for doing anything like this.

With the null_pointer_is_valid mentioned, it could be concievably implemented, even by just generating a trampoline function that performs the volatile read/write and has the attribute when support of this feature is warranted for a particular target in rustc.

Having the ability directly on particular operations would be useful. I can look into bringing that up.

2 Likes

I haven't done a lot of embedded work myself, but my understanding is that when there is architecturally-defined data at absolute address zero, a standard workaround for languages that insist on reserving that bit pattern for "the" null pointer is to declare an external symbol for the data, and then define that symbol to live at address zero in a linker script. This prevents the compiler from mis-optimizing accesses, since it has to assume that the symbol has an unknown, valid, address. Other usage may have surprising behavior (e.g. comparisons of this symbol to null may or may not evaluate true depending on how clever the compiler is today) but I am under the impression that it usually isn't a problem.

This should be possible with Rust today, so, are there things that your proposal would get you that this wouldn't?

1 Like

Rust assumes a pointer that is bitwise equivalent with 0usize cannot be used to validily access any object. This has the particular noted effect to making references invalid for such a value. Presently, with this behaviour, trying to stick that reference into an Option<&T> may just turn that option into a None (particularily, if the Option<&T> is then subsequently accessed from a point which is opaque to those optimizations). The current stance of rust, and in particular the unsafe coding guidelines, is that even producing a null &T or NonNull<T> is undefined behaviour, even if you don't use it any further (see Why even unused data needs to be valid). Additionally, llvm may make even further blind assumptions about an access to address 0 (given the fact it thinks it cannot be done in most cases).

The avoidance of undefined behaviour that isn't assigned meaning by non-standard extensions in use, even though it will probably work, is an absolute requirement of all of my code. On principle, I do not write code that depends on undefined behaviour to evaluate properly, because inheriently, that code is broken beyond all consideration. Even if it will probably work, that doesn't mean it won't break at some point in the future, and break heavily, to the point where all of the code relying on it is now entirely non-functional.

2 Likes

Let me make sure I understand what you're saying here: in this fragment, get_interrupt_table might appear to return None from the perspective of its caller in another crate?

#[repr(C)] pub struct InterruptHandler { ... }
pub type InterruptTable = [InterruptHandler; 64];

extern {
    // defined to live at address 0 by linker script
    static mut interrupt_table: InterruptTable;
}

// has to be Option<&mut T> to match external convention, but
// on this platform always returns Some(&mut interrupt_table)
pub fn get_interrupt_table() -> Option<&'static mut InterruptTable> {
    unsafe {
        Some(&mut interrupt_table)
    }
}

That is clearly a serious problem with the approach I suggested, but I don't see how it's not also a serious problem with the approach you originally suggested. Can you explain please?

Yes, and in fact, most likely will due to how Option<&T> is defined (however, with undefined behaviour, there cannot be any definate reasoning about it, by the very nature of undefined behaviour) From core::option - Rust

This usage of Option to create safe nullable pointers is so common that Rust does special optimizations to make the representation of Option < Box<T> > a single pointer.

The mentioned optimization is mandatory, in particular. It is stable that it is valid to pass Option<&T>, Option<fn(...)->...>, and even Option<Box<T>> (though the last one may not do what one expects) to FFI code that accepts a T*/const T* argument. It is also stable that core::mem::transmute::<usize,Option<&T>>(0).is_none() is true, and is neither a compile-time error, nor Some(&*(0usize as *T)), whatever value &*(0usize as *T) may be (as I previously mentioned, predicated on T: Sized). (This is notwithstanding the obvious issues of returning a &mut to static memory from a safe function, which is incredibly unsound). Allowing the implementation to choose the null pointer value would solve this issue, though as the behaviour I mentioned is stable, it could not be done before Rust 2021, which could define the result of core::ptr::null::<T>() to be a pointer which is bitwise equivalent to an implementation-defined value of type usize, and is guaranteed to compare unequal to any valid pointer to any object (similar to how it is defined in C/++).

In my proposal, I narrow the scope of validity to only the volatile operations, and only through raw pointers. So in your case, core::ptr::read_volatile(raw_const!(interrupt_table)) would become valid iff the particular implementation supported it. Same with core::ptr::write_volatile(raw_mut!(interrupt_table),new_table).

1 Like

@zackw your approach relies on actually causing UB (by using 0 as the real address for operations that do not support 0), but then using tricks to prevent the compiler from exploiting that UB (the linker script). I wouldn't call that a "solution", but rather a hack. @InfernoDeity is looking for a proper solution without such hacks.

Can we get this engraved on a plate or so please? :smiley:

5 Likes

The solution is to use inline assembly to read/write from address zero.

3 Likes

It can be unreasonable to expect to need to use inline assembly. What if the thing you are accessing is, in fact, a symbol, though you need volatile access to it anyways. And what if that thing happens to be at address 0? Also, the ordering of memory operations of inline assembly, is underspecified from what I've read, w.r.t. atomic operations. Can I prove that an atomic fence is sufficient to allow inline assembly to synchronize access to this address?

1 Like

Aha, I get it now. It's not that your proposal can do more stuff, it's that it intentionally can do less stuff in order to facilitate making it well-defined.

Unfortunately, in LLVM, the interactions of volatile accesses and atomic accesses are also unspecified.

Rust adopts the C/++11 memory model as correct, IIRC. An atomic fence is going to have at least some form of memory synchronization, given one cannot have a relaxed atomic fence. (And I believe Rust does not have the consume ordering)

Most architectures have long used 0x0…0 as an invalid or trapping virtual address. So as a practical matter, which legacy architectures put something essential at physical address 0x0…0, and do not provide an alternate way of mapping a non-zero page to that address once the initial boot code β€” presumably written in assembly β€” has run? Do any of those architectures even have atomic operations?