11
$\begingroup$

In Rust, creating a raw pointer is allowed in safe code (i.e. outside of unsafe blocks), but then dereferencing it is unsafe:

let my_num_ptr: *const i32 = &my_num;
let value: i32 = unsafe {*my_num};

Undefined behaviour would only occur in a program which both creates and incorrectly dereferences a raw pointer. So long as one of these operations is only allowed in an unsafe block, memory safety can be guaranteed for programs which have no unsafe blocks.

What if, instead of requiring an unsafe block for the dereference operation, it is instead required for the creation of the raw pointer? For example:

let i: const *u32 = unsafe {&4};
let b = *i;

Or even:

let i: mut *u32 = unsafe {&mut 8};
*i = 5;

In this hypothetical language, operations which create a raw pointer are unsafe (and must only occur in unsafe blocks), but dereferencing them is allowed in safe code, on the basis that it's the programmer's responsibility to make sure when creating the raw pointer that the pointed-to value will stay alive at that memory address for long enough. Is this sound? Would there be any advantages to doing it this way instead of the Rust way?


Note that in Rust, a unsafe block does not mean that the entire program is unsafe, just that there are invariants the compiler can't check, so the programmer is responsible for them. If the programmer has made sure the rules are followed, it's perfectly safe.

In Rust currently, it is undefined behavior to dereference a null pointer, a unaligned pointer, or one that points to uninitialized memory. I'm suggesting that the undefined behavior happens when constructing such a pointer instead.

$\endgroup$
14
  • $\begingroup$ I think this entirely depends on how you can get a raw pointer in the first place right? If the only way is unsafe {&N } (for some N) then of course there $\endgroup$ Jun 16, 2023 at 20:47
  • $\begingroup$ The only way to get raw pointers is manually constructing them, which is unsafe, or calling a foreign function which is already unsafe anyways. In both these cases the programmer is responsible for the invariants. $\endgroup$
    – mousetail
    Jun 16, 2023 at 20:48
  • 2
    $\begingroup$ If constructing them is unsafe then the program is already unsafe. I'm not sure it's meaningful to ask if a different operation in an unsafe program is safe; a program having undefined behaviour means that all operations that program performs can do anything, even operations which can occur outside of unsafe blocks. $\endgroup$
    – kaya3
    Jun 16, 2023 at 20:52
  • 2
    $\begingroup$ I think I see what you mean...so we sort of have three levels here, safe-safe, unsafe-safe (it's safe, but the compiler can't tell on its own), and unsafe-unsafe. And so you're asking, assuming we're in the second category, the pros and cons of requiring the unsafe block at creation vs. dereferencing? $\endgroup$ Jun 16, 2023 at 20:55
  • 1
    $\begingroup$ Upon re-reading the question, I think I now understand what you're asking. I'm going to make an edit which I hope clarifies your intent. $\endgroup$
    – kaya3
    Jun 16, 2023 at 20:58

4 Answers 4

16
$\begingroup$

The trivial answer is yes, it's sound so long as the programmer keeps up their end of the bargain and doesn't create and then incorrectly dereference a raw pointer; and so long as one of those two operations is considered "unsafe" and must be put in an unsafe block, then programs with no unsafe blocks cannot violate memory safety in this way.

But I think that isn't a satisfactory answer to the question, because it doesn't address what burden is placed on the programmer to ensure that they use this pair of operations together safely.

In Rust currently, it is undefined behavior to dereference a null pointer, a unaligned pointer, or one that points to uninitialized memory.

I think the last point needs to be addressed. "Uninitialised memory" doesn't just mean memory which hasn't had a valid value written to it. For our purposes here, memory is also uninitialised if the allocation has been freed before the dereference occurs (i.e. use-after-free), even if it was initialised before it was freed.

I mention this not because it should be new information, but to highlight what I think is a significant problem with your idea. You can guarantee when the pointer is created that it is non-null, correctly-aligned and that the data it points to is initialised; but you can't guarantee at the creation-site that it will stay initialised for long enough. (Well, you could bind the lifetime of the allocation to the lexical scope it's allocated in, but that gives you the borrow checker, not raw pointers.)

To avoid use-after-free without a borrow checker, it is the programmer's responsibility to check each time the pointer is dereferenced that the dereference can occur only after initialisation and before freeing. That is, the programmer's burden is to ensure this at each dereference site. So it makes more sense to put the unsafe block around the dereference sites (as Rust does), because that's where your attention needs to be when you're checking that the pointer is being used correctly.

In contrast, if there is just one unsafe block where the pointer is created, then there is potentially a large amount of code outside of that block which also needs to be audited, but no "warning signs" telling you to look there.

$\endgroup$
1
  • $\begingroup$ I was approaching the question from the standpoint of raw pointers which can only be created to regions of address space which exist outside the "normal" semantics of the language, and which a program will be allowed to access for as long as it is running. While this wouldn't accommodate all use cases associated with raw pointers, the possibility of "use after free" won't exist if there's no concept of "free". $\endgroup$
    – supercat
    Sep 26, 2023 at 21:32
3
$\begingroup$

In Rust currently, it is undefined behavior to dereference a null pointer, a unaligned pointer, or one that points to uninitialized memory.

It's also undefined behaviour to.

  1. Access a block of memory after it has been deallocated.
  2. Violate the aliasing/mutability rules.

The first is bad, it essentially means that any pointer created based on a memory with non-static lifetime is a time-bomb.

The second though is arguably worse. Consider for example the following code.

fn main() {
    unsafe {
        let mut a : String = "hello world".to_owned();
        let b : * mut String = &mut a;
        let c = (*b).as_str();
        *b = "crash and burn baby".to_owned();
        print!("{}",c);
    }
}

Normally deriving a &str from a String is safe. It's safe because not only is the String gauranteed to outlive the &str, but the String is guaranteed to remain in a stable state for as long as the &str exists.

In this case though, while the String does outlive the str it does not remain in a stable state for the lifetime of the str. When the String is reassigned to a new value, the str derived from it becomes a dangling reference!

Or to put it another way, no matter how much care is taken by the creator of a *mut String, the code derefencing that *mut String can invoke a serious violation of memory safety.

$\endgroup$
1
$\begingroup$

A language with a pointer type that disallows pointer arithmetic, and a "fat pointer" type which includes a base address, range, and offset, could uphold memory safety if the only "unsafe" operations were the formation of the above pointer types, in contexts where the address ranges associated with pointers would remain usable for the duration of the program. Only address ranges that were not otherwise used by the application could have such pointers constructed to them, and any special functions that generated such pointers without being marked "unsafe" would need to ensure that they only returned pointers to storage meeting the aforementioned criteria, but many tasks that would otherwise require raw memory access could be accomplished even with such restricted semantics. For example, a system which uses a memory-mapped display may be able to allow "safe" application code direct access to the display memory if it will be entitled to have exclusive access to that memory for its lifetime.

$\endgroup$
-2
$\begingroup$

The secret is that most of the practical memory safety comes from bounds checking of arrays. So if you have that you get 80 % of the benefit while still allowing use of pointers. Odin is a good example of this in practice, http://odin-lang.org/.

$\endgroup$
12
  • $\begingroup$ This doesn't really answer the question, which is about whether memory safety can be fully ensured. This approach still allows dangling pointers. $\endgroup$ Jun 23, 2023 at 15:53
  • $\begingroup$ Those are only a problem if you have a long running application. And if you do have that, there are techniques that make it very easy to not make those mistakes. rfleury.com/p/untangling-lifetimes-the-arena-allocator $\endgroup$ Jun 23, 2023 at 15:55
  • 3
    $\begingroup$ Dangling pointers can still happen even without freeing heap memory if they're pointers to the stack. Similarly, this does nothing about wild pointers or null pointers (though they'll often segfault anyways on most systems). $\endgroup$
    – Bbrk24
    Jun 23, 2023 at 19:54
  • 1
    $\begingroup$ "If you use the right technique" you don't know programmers? The only way to make programmers use the right techniques is either to make the compiler force them to (as Rust does) or make it illegal :P $\endgroup$
    – Seggan
    Jun 24, 2023 at 3:43
  • 1
    $\begingroup$ To LQP reviewers: This doesn't meet the criteria for either NAA (it does still attempt to answer the question) or VLQ. $\endgroup$ Jul 1, 2023 at 22:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .