Skip to content

What about: Targets where NULL is a valid pointer #29

Closed
rust-lang/rust
#141260

Description

@RalfJung

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.

Activity

  1. gnzlbg commented on Sep 18, 2018

    @gnzlbg
    Contributor

    Relevant: https://stackoverflow.com/questions/27714377/does-standard-define-null-pointer-constant-to-have-all-bits-set-to-zero

    C is very explicit that a null pointer must be a constant expression:

    An integer constant expression with the value 0, or such an expression cast to type void*, is called a null pointer constant. [...] NULL expands to an implementation-defined null pointer constant.

    This allows the compiler to replace 0 with whatever the null pointer value is for the target (e.g. there were some targets where this was the case https://stackoverflow.com/a/2597232/1422197). That is, reading zero from stdin into an integer, and casting that integer into a pointer, does not necessarily make that integer a null pointer.

  2. RalfJung commented on Sep 18, 2018

    @RalfJung
    MemberAuthor

    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 GEP inbounds), pointer arithmetic is restricted to inbounds pointers; loads/stores are further restricted to dereferencable pointers.

    In LLVM, a pointer is "inbounds" even when it pointers to (within the bounds of) a dead (deallocated) allocation, that's fine for GEP inbounds. Naturally, for "dereferencable", we only consider allocations that still exist. C doesn't let you do anything with pointers to dead allocations so the question does not come up.

  3. gnzlbg commented on Sep 18, 2018

    @gnzlbg
    Contributor

    That is just a matter of how NULL is represented on the machine, though?

    In the C virtual machine NULL is just an address that never points to a valid object and compares unequal to any address that does. Since this address cannot be dereferenced by a valid C program, the C standard doesn't care about what happens when a program does so.

    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 /proc/sys/vm/mmap_min_addr. The C standard does not require 0x0 to be a sentinel address, it requires one such address to exist, and that address is NULL. Using C, a user could set the sentinel address to 0xffff and make use of 0x0 as a valid address. On Linux, accessing 0xffff would then still properly raise a SIGSEV and catch null pointer dereferences.

    The question is, do we want to allow Rust to be used on applications that have to write to address 0x0 ? If we hardcode 0x0 as the NULL address, then this becomes impossible. If we say that such an address must exist, this address is ptr::null(), integer representations of it can be obtained by casting that address to isize/usize (and have a {usize,isize}::null_ptr() integer constants for it), and ptr.is_null() is the only way to properly check whether a pointer is null, then this would remain possible (as in, one could improve the rust toolchain to be able to do that without ending up with a language that is not Rust).

  4. hanna-kruppe commented on Sep 18, 2018

    @hanna-kruppe

    Using C, a user could set the sentinel address to 0xffff and make use of 0x0 as a valid address. On Linux, accessing 0xffff would then still properly raise a SIGSEV and catch null pointer dereferences.

    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.

  5. gnzlbg commented on Sep 18, 2018

    @gnzlbg
    Contributor

    Though keep in mind that this requires cooperation from the C toolchain

    Yes, since the C std states that NULL is a 0 integer constant expression, the translation to 0xffff has to happen in the toolchain.

    I personally think that how it would work out in C is very weird. For example, one cannot create a ptr to 0x0 by just writing char* ptr = 0; (that would create a null pointer). So the only appropriate way to crete a pointer to 0x0 would be to do so from a call to malloc that returns it or similar. Weirder is that char ptr* = 0xfff wouldn't be a NULL pointer because that's not a NULL constant expression.


    I think Rust could do much better here using ptr::null() / ptr::null_mut() and ptr.is_null() as the only ways to create null pointers and test for null-pointer-ness.

  6. RalfJung commented on Sep 18, 2018

    @RalfJung
    MemberAuthor

    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.

    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.

  7. hanna-kruppe commented on Sep 18, 2018

    @hanna-kruppe

    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 -fno-delete-null-pointer-checks has brought up the idea of using a non-0 address space for all pointers. This exploits the property that address 0 is only assumed to be invalid in that address space, not in others, so it's automatically correct (modulo existing bugs in handling of other address spaces).

  8. gnzlbg commented on Sep 18, 2018

    @gnzlbg
    Contributor

    Just to state the obvious, this is far from reliable -- compilers can still exploit NULL-deref in any way that want.

    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.

    we first have to make some fundamental changes to LLVM.

    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 0x0 until LLVM supports them, if ever.

    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-0x0 null pointers for some target to target such a system. Somebody that has to target such a system is going to already be in a very bad place to begin with, and that we should do everything we can to make their lives easier as long as that doesn't impacts the 99.99% of users.

    I would perfectly fine with just saying that the only valid way to check whether a pointer is null is to call ptr.is_null(). And if you want to check whether an isize value corresponds to a null pointer, you would have to cast it into a pointer and then call ptr.is_null() on it. We could also guarantee that casting ptr::null()/ptr::null_mut() into an isize/usize and then casting that isize/usize back to a pointer and calling ptr.is_null() always returns true.

    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 clippy, rustc warnings, and miri, to be able to warn and detect null pointer checks that are not "portable" (e.g. ptr as isize == 0 => "use ptr.is_null() instead"). We could always extend this "minimal null ptr" proposal to allow ptr as isize == 0 as a valid null pointer check later on if we end up needing that.

  9. RalfJung commented on Sep 18, 2018

    @RalfJung
    MemberAuthor

    We also have enough tooling already in the form of clippy, rustc warnings, and miri, to be able to warn and detect null pointer checks that are not "portable"

    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 ptr::null() (those functions should likely panic or so on such targets). And the examples we have seen -- real-mode x86 and other similarly low-level use-cases -- AFAIK have no sentinel value, so we'd not even doing them a service by supporting 0xFF as sentinel.

  10. gnzlbg commented on Sep 18, 2018

    @gnzlbg
    Contributor

    But from what I can see, the C version of this is "add some compiler flags and hope for the best".

    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, null is a null pointer, because it was constructed from a 0 integer constant expression. null2 is also a null pointer, because NULL expands to a 0 integer constant expression. But not_null is just a pointer to address 0x0.

  11. RalfJung commented on Sep 18, 2018

    @RalfJung
    MemberAuthor

    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.

  12. alercah commented on Sep 18, 2018

    @alercah

    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, not_null being a null pointer is the correct behaviour (and reliable, if the implementation specifies that representation).

    Everything Rust does currently is, I think, theoretically compatible with a platform where it is not 0; the only questions are:

    1. Whether we want to consider the potential of supporting platforms with non-zero null at some point.
    2. Whether we want to force everyone to work their code around that assumption (one way might be by having CTFE use other null values, for instance, so that we can trap on people who break this)
  13. parched commented on Sep 20, 2018

    @parched

    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.

    I tend to agree and I think this is basically what GCC does with -fno-delete-null-pointer-checks.

  14. parched commented on Nov 22, 2018

    @parched

    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 null-pointer-is-valid field to the target spec and pass this through. The big part would be conditionalizing all null assumptions/optimisations in rustc.

    If there are any non-zero null targets, they could just use null-pointer-is-valid too, it would just be a pessimization.

  15. 38 remaining items

  16. added
    A-memoryTopic: Related to memory accesses
    S-pending-designStatus: Resolving this issue requires addressing some open design questions
    and removed
    C-open-questionCategory: An open question that we should revisit
    on Jun 6, 2023
  17. Sympatron commented on Dec 25, 2023

    @Sympatron

    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.

  18. Lokathor commented on Dec 25, 2023

    @Lokathor
    Contributor

    Inline assembly.

  19. added a commit that references this issue on Jul 19, 2025
  20. added a commit that references this issue on Jul 20, 2025
  21. added a commit that references this issue on Jul 20, 2025
  22. added a commit that references this issue on Jul 20, 2025
  23. added a commit that references this issue on Jul 20, 2025
  24. added a commit that references this issue on Jul 21, 2025
  25. added a commit that references this issue on Jul 29, 2025
  26. gkgoat1 commented on Oct 30, 2025

    @gkgoat1

    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.

    +1, also with wasm multi memory where multiple independent memory spaces can be created and used simultaneously. Both would require llvm support though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-memoryTopic: Related to memory accessesA-validityTopic: Related to validity invariantsS-pending-designStatus: Resolving this issue requires addressing some open design questions

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions