-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What about: Targets where NULL is a valid pointer #29
Comments
C is very explicit that a null pointer must be a constant expression:
This allows the compiler to replace |
That is just a matter of how NULL is represented on the machine, though? Even in C, there still is a sentinel value of a pointer that can never be valid (inbounds or dereferencable), and that sentinel value is called NULL. In terms of terminology I am using here, an address is inbounds of an allocation if its offset within that allocation is no larger than the size of the allocation. It is dereferencable if the offset is less than the size. The key difference is that the first byte after the end of an allocation is inbounds, but not dereferencable. In C (and LLVM In LLVM, a pointer is "inbounds" even when it pointers to (within the bounds of) a dead (deallocated) allocation, that's fine for |
In the C virtual machine Hardware, OTOH, does not need to have an invalid address (why would it). When an OS allocates virtual memory pages for a process, starting at a virtual address 0x0, what most OSes do is mark the 0x0 address as an invalid address, for example, by protecting access to it from user space, or not mapping it to any physical memory, such that the CPU raises an exception when the address is dereferenced (e.g. on POSIX you would get a SIGSEV, Windows use SEH, etc.). That is, on most OSes, and on most hardware, the OS and the hardware cooperate to let the users know that their C programs are broken. What happens when the hardware does not support memory protection, virtual addresses, etc., and the OS cannot let the user know about the errors in the programs (if there is an OS at all) ? From C point-of-view, it doesn't matter, those programs are illegal, undefined behavior is undefined, etc. The real question is what happens when the user wants to write to the 0x0 address for "reasons". On Linux, you can easily disable memory protection for the 0 address if you really need to do that by modifying The question is, do we want to allow Rust to be used on applications that have to write to address |
Though keep in mind that this requires cooperation from the C toolchain (compiler, libraries, debuggers, etc. – AFAIK neither GNU nor LLVM make any effort to support this in the least) and is an ABI breaking change, so the odds of being able to actually doing this in Linux or any other established environment are practically nil. |
Yes, since the C std states that I personally think that how it would work out in C is very weird. For example, one cannot create a I think Rust could do much better here using |
Just to state the obvious, this is far from reliable -- compilers can still exploit NULL-deref in any way that want. LLVM will optimize pub fn foo() {
unsafe {
*(0usize as *mut i32) = 42;
}
} to "unreachable", but will compile pub fn foo() {
unsafe {
*(8usize as *mut i32) = 42;
}
} to a store at address 8. If we want to support targets with other representations of the NULL pointer in Rust, we first have to make some fundamental changes to LLVM. |
I'm not so sure about that. Recent llvm-dev discussion about supporting the GCC option |
Note that I was talking here about what happens only if machine code dereferences a null pointer independently of what optimizations Rust and LLVM might do.
FWIW I think that it would be perfectly fine for Rust with the LLVM backend to not support targets in which a null pointer is not What I am unsure of is whether we want to make supporting those targets impossible for a sufficiently motivated party. For example, mrustc can compile Rust to C, which could be used with a C toolchain that supports non- I would perfectly fine with just saying that the only valid way to check whether a pointer is null is to call I think that would be a reasonable way of handling null pointers in unsafe rust for the 99%, while still allowing interested parties to target weird platforms using Rust instead of just giving up and using C instead. We also have enough tooling already in the form of |
I am not sure sure about that... But I think this discussion is very hard to have in the abstract, it'd be easier if there was a backend for Rust or even C that properly supports this and that can be looked at for how they handle this. But from what I can see, the C version of this is "add some compiler flags and hope for the best". TBH, I'd find it much more sane to support targets with no sentinel value on pointers than targets where that value is non-0. That would "just" remove a whole bunch of assumptions from the optimizer, miri and maybe other places, but it wouldn't have to mess with the definition of |
I don't think compiler flags are needed. The reliable way to create a pointer in C to a fixed address is to just not use a constant expression: // null pointer:
char* null = 0;
// also a null pointer:
char* null2 = NULL;
// not a null pointer:
int value = 0;
char* not_null = (char*)value; Here, |
I understand what the standard says about this, but given that the IRs on which compilers do their optimizations are so loosely specified, I am doubtful that this distinction can even be meaningfully made there -- and that all optimizations follow it correctly. As far as LLVM is concerned, I very strongly doubt it makes any difference between your examples. Godbolt agrees. |
I believe that the correct reading of the C standard is that one or more bit representations of pointer types are considered to be null pointer values, and imposes restrictions on them accordingly. The exact representation is implementation-defined or unspecified (the language in the standard is a tad fuzzy on this point, but I don't think it matters). So on a platform where the null pointer's representation is indeed all-zeroes, Everything Rust does currently is, I think, theoretically compatible with a platform where it is not 0; the only questions are:
|
I tend to agree and I think this is basically what GCC does with |
BTW, not sure how I missed this before, but LLVM supports this since version 7 https://reviews.llvm.org/rL336613, so it would be quite simple to add a similar If there are any non-zero null targets, they could just use |
Here's two code examples to boil this down to. Are the following three examples guaranteed to be true, currently? let p: *const () = 0_usize as _;
assert!(p.is_null());
assert!(p == std::ptr::null()); Also, is this guaranteed to work? assert_eq!(x, 0_usize);
let x: Option<&T> = unsafe { transmute(x) };
assert!(x.is_none()); Seeing a lot of the documentation trying to always refer to null instead of zero and a lot of API built around the hide the knowledge that the null pointer is defined as |
@skade The API docs of
(emphasis mine) I'm not sure if this is by design or whether that's an oversight that should say non-null instead. That wording was introduced here. AFAICT, the reference doesn't say what NULL is, but @gankro's post says:
|
In current rust null must be zero for NonNull to work. Internally it declares the base value of the type to be 1 instead of 0, and that's all it does. So the niche to get the null-pointer Option optimization must be 0. Then, for Option<&T> and Option<&mut T> to be bitwise compatible they also must use zero as the null value. It might be possible to change that in the future, but it would be a relatively massive change that would require rechecking huge amounts of code to fix up places where people's old assumptions need to be updated. |
Also apparently the C spec is a wacky "the null pointer must always compare equal to the integer expression 0, and only equal to the pointer expression NULL, but doesn't have to literally be the all-zero bit pattern." |
@Lokathor this is all clear, but the question is if that's just the current implementation or if people can rely on it. A lot of the things documented here somehow work, but are actually undefined.
This is circular, as this discussion has sparked from precisely that statement. ;) I mean, the option to make that a language guarantee is fine, but I can't find any source for this. |
I don't think relying on it is a good idea. |
@skade Currently it is in what most would call the "implementation defined" area. Null is absolutely and definitely 0, no worries of UB. However, a new release of the compiler could potentially change that (with sufficient work done), so I would not call it an eternal guarantee. |
Citation needed. Many comments in this thread argue otherwise. |
Sorry, in rust null cannot be any value other than 0. In C it can be anything it chooses, and if the local C picks a non-zero value then Rust will just not have the same null. |
For what it's worth, I don't know of any C environments where NULL is not 0 that are not obsolete architectures from the 70's. There are environments where 0 is a valid pointer at the assembly level, and the contents of memory there are important: for example, on old ARM systems, the CPU starts executing code at address 0 when it's powered on. But 0 simultaneously serves as a null pointer for C programs written in those environments. This works because there's typically no need to access that address from C anyway. |
The only thing approaching an example that I am aware of is pointers to member functions of virtual classes in C++ under Microsoft's ABI. But this is not C nor it is it a plain pointer. |
That's the only use case I know as well. Maybe we should try to collect more use cases? For a restricted set of use cases, the best solution might be very different from "allowing NULL to be an implementation-defined address". For example, for putting code at address 0, we could maybe support an option on |
If people are wanting to put code at 0, that sounds like linker script work. for reading and writing 0, i believe that inline asm can already do this? |
@Ixrec To my reading, it's safe to assume that rustc > 1.0 performs this optimisation. The Nomicon is a practical document though, not a spec. But when taking the Nomicon as a reference, it also clearly says:
And Option is not repr(C). (https://doc.rust-lang.org/nomicon/transmutes.html) On the other hand, also:
as of writing. |
Note that this statement:
is definitely an incorrect over-simplification in many respects unrelated to the |
@skade see https://rust-lang.github.io/unsafe-code-guidelines/layout/enums.html for our current thinking on enum layout guarantees. in particular this section. |
disclaimer: I have no deep knowledge of compilers and I think C is irrelevant to what rusts semantics here should be, past being something to inform things. My two cents are that it would be a shame if "arbitrary" restrictions precluded using rust in obscure low level use cases which violate common conventions (that are not technical necessities). Or is that out of scope for rust? I'd have to do a deeper review - so I don't know exactly what happened - , but I believe Christopher Domas made 0x0 a valid memory address as part of the techniques he developed for Sandsifter ( https://github.com/xoreaxeaxeax/sandsifter ) and the accompanying research; https://www.blackhat.com/docs/us-17/thursday/us-17-Domas-Breaking-The-x86-Instruction-Set-wp.pdf , it would be nice if things like this were possible in rust.
|
You could still use assembly to read and write 0 in the odd case where it's important to be able to. |
To clarify a little, what I'm trying to say is should specific memory addresses really be treated as special by necessity? |
It's a small optimization but it comes up so often that it's worth it |
Hopefully not repeating anything, but in WASM, there are no restrictions/special behaviour around address 0. Additionally, WASM uses a Harvard architecture, so there will be a function address (or more accurately, index) 0. I believe this is handled in llvm by emitting a dummy function at the 0th index function, but it is still worth mentioning. |
Just passing by with a real-world system with valid address 0 that actually has a reason to be written to. Microcontrollers in STM32H75x/H74x (ARM Cortex-M7 CPU) series have quite an interesting memory map with 8 different RAM memories. One of them is called ITCM, it spans 64kB from address 0 to 0xFFFF. This memory is dedicated to instructions as it's the only memory that can be as fast as the CPU (for reading instructions). As it is RAM memory it needs to be initialized. Not only that but in some applications I actually want to override all/parts of it when entering certain program states. Fortunately this is memory region targeted towards instruction and not data, so pointers to that region are quite rare. |
@enbyted Another real-world system that has a reason to write to address 0x0: bare metal code on ARMv6. Or more generally, bare metal code which doesn't use virtual memory on ARM systems that don't have a VTOR or VBAR register to specify the location of the interrupt vector table. The vector table is either at address 0x0 or 0xFFFF0000; if you have virtual memory it's easy to map 0xFFFF0000 to the page where the table is, but if you are operating with only physical memory you need to put the table at 0x0. One example of where/when you do this: bare metal code on the Raspberry Pi (1). The Pi places the kernel image at 0x8000, and your code needs to copy the interrupt table to 0x0. This is what we have students do in a class I teach. |
I actually designed what I think to be the least problematic solution. Whether or not rustc supports it would be on a target-by-target basis. For completeness, conditionally-supported means that a particular implementation may support it, and documents when it does not, and implementation-defined means the implementation chooses how to (it is a parameterized part of the abstract machine), and documents how it makes the particular choice. |
In terms of NULL accesses, it certainly makes sense to treat them the same as OOB accesses (Cc #2). This also recently came up on Zulip, and people were in favor of permitting volatile accesses (and only volatile accesses) to do that -- but it is unclear if that will be possible with LLVM. |
As mentioned (and I believe you brought up originally on the Pre-RFC), there is an llvm attribute for this (though it is #[llvm_attribute(null_pointer_is_valid)]
pub fn read_volatile<T>(ptr: *const T) -> T{
<load volatile>(ptr)
} where |
Ah right, LLVM has that for NULL specifically... maybe they would be open to adding another attribute that removes all inbounds assumptions (not just NULL).
Or we could just adjust how the intrinsic is codegen'd -- that seems simpler? |
That would be the same yes. The difference is that either, the validity of null leaks to the surrounding code (so now non-volatile accesses wouldn't be optimized for null pointers), or that the intrinsic would have to emit (or be emitted as) a trampoline. This can obviously be done conditionally, chosing the most optimal one, or letting llvm decide if it wants to inline and extend the scope of the attribute. |
Oh, this is a per-function attribute in LLVM? One more reason to propose a change -- something that works per-access seems more useful here. |
Yes, it's per function as far as I know (this was brought up in the Pre-RFC). |
On the Commander X16 (whose 65C02 CPU is currently not targeted by upstream Rust, but can still be targeted fairly easily by adding a custom target), addresses 0 and 1 control banking for RAM and ROM, and since the RAM window in the address space is fairly small, you would expect to change the bank a lot even from Rust code. Avoiding inline assembly for this would be useful because it makes sure that the bank number can be an immediate, and opens it up to optimizations. |
I don't think you can avoid inline asm. You need something with sideeffects such that the compiler doesn't assume that no banks have been switched. A regular or volatile store is not enough even if the address wouldn't be zero. You need something that can have arbitrary sideeffects in the eye of the optimizer, including reading and writing all memory that is bank switched. That would either be a regular function defined in a different codegen unit so the optimizer can't peek inside it to calculate which sideeffects it has ( |
Also, all mutable and shared references would be invalidated across such a bank switch. |
I don't think all would be, necessarily—just those pointing to the high RAM area which is banked. You could use raw pointers for that region of memory, or a custom pointer type which switches the banks when dereferencing, while reserving normal references for low RAM. But yes, better support for non-flat address spaces would be nice to see in Rust. But perhaps that's getting too off-topic from writing to address zero being useful. |
Sorry, I somehow understood that all memory is being switched on bank change. |
Is there any way to read or write from address 0 today with Rust? Since address 0 is valid on all ARM-Cortex-M microcontrollers I know of, I think this should be made possible. Optimizing access to 0 away should be disabled in general on theses targets IMO. |
Inline assembly. |
This recently came up in a discussion. To my knowledge, LLVM has a pretty hard-coded assumption that for address space 0, NULL is never inbounds (let alone dereferencable), so we cannot actually support such targets with the LLVM backend. But there may well be tricks I am not aware of.
The text was updated successfully, but these errors were encountered: