|
|
Log in / Subscribe / Register

Mixing safe and unsafe

Mixing safe and unsafe

Posted Oct 30, 2025 9:20 UTC (Thu) by matthias (subscriber, #94967)
In reply to: Mixing safe and unsafe by tialaramex
Parent article: Fil-C: A memory-safe C implementation

> In January Rust 1.84 shipped its provenance APIs and all the associated documentation went from speculative to de facto how Rust works.

Thanks. I somehow missed that.

>You're wrong about the special-ness of one-past-the-end in Rust. It's not special in Rust, it's just one past the end.

The provenance documentation says it is different from 16 past the end, as it is still inside provenance. Just the same as in C.

> If you're thinking "I would use a dereference" Bzzt, that's going to be a problem. unsafely *ptr = some_goose; will try to destroy the previous goose, but there is no goose, just uninitialized memory so that's UB.

Only if goose implements drop. But then you are (implicitly) creating a &mut to the uninitialized memory, which is indeed UB. And usually the drop handler will read the memory, which is also UB. This is not really a difference in pointer semantics but more a difference on how the assignment operator works. I do agree that ptr.write() should be used, if there is no valid object that you want to drop, even if the type does not implement drop. It is much more obvious that the programmer wants to do a write and not an assignment this way.

Of course there are differences in pointer handling between the languages. I think of pointer comparisions which can be UB in C(++), while they are part of safe rust and thus must not cause any UB. These are details that of course need to be accounted for when writing actual code. However, when thinking of raw pointers in rust, they are much more similar to C pointers than to anything else in the rust language. They always feel somhow alien in the rust language. So having an expressive API with methods like offset and write is a good thing. Of course, it is still unsafe, but less error prone.


to post comments

Mixing safe and unsafe

Posted Oct 30, 2025 13:15 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (3 responses)

Where are you seeing the claim that one-past-the-end is privileged in this way? I can't see it in anything I reviewed, but I only briefly flipped past because I'm on a lunch break.

Likewise I didn't find a claim that *ptr = some_goose; is sound when Goose is not Drop. I can see in principle how this could be arranged, but I couldn't think of any reason I would want it, and since we're unsafe if it isn't legal the compiler isn't going to necessarily point out the problem, so if it is legal I want a URL to drop in an adjacent safety comment so people know why I thought it was OK to write this.

Mixing safe and unsafe

Posted Oct 30, 2025 13:38 UTC (Thu) by matthias (subscriber, #94967) [Link] (2 responses)

From the provenance section of https://doc.rust-lang.org/std/ptr/index.html :
> It is undefined behavior to access memory through a pointer that does not have provenance over that memory. Note that a pointer “at the end” of its provenance is not actually outside its provenance, it just has 0 bytes it can load/store.

"at the end " refers to what in C would be called one past the end. If it would point to the last element, the size would not be zero, so they really mean one past the end.

I am not actually sure whether there is a real difference to being outside of the provence, as you cannot load or store anyway. In C, there is a difference as comparison operators take provenance into account. In rust, comparison operators are only comparing the address. So there might not be a real difference.

From the documentation of pointer https://doc.rust-lang.org/std/primitive.pointer.html :
> Storing through a raw pointer using *ptr = data calls drop on the old value, so write must be used if the type has drop glue and memory is not already initialized - otherwise drop would be called on the uninitialized memory.

It is not explictly stated that you are allowed to store non-drop values in this way. However, if you would not be allowed to, this would be phrased differently. I still would use write for uninitialized memory, as it looks cleaner.

Mixing safe and unsafe

Posted Oct 30, 2025 14:07 UTC (Thu) by notriddle (subscriber, #130608) [Link]

> I am not actually sure whether there is a real difference to being outside of the provence, as you cannot load or store anyway.

You can subtract from it to increase the size. The backwards-iterator works that way, subtracting from the pointer and then reading it.

Mixing safe and unsafe

Posted Oct 30, 2025 16:24 UTC (Thu) by tialaramex (subscriber, #21167) [Link]

Thanks for those excerpts.

I agree the terminology is confusing to a C programmer, who is used to thinking of pointers as always pointing to at least one whole byte of RAM, because Rust's pointers (even valid ones) don't necessarily do that (we can ask for a pointer to a single empty tuple, or indeed to an array of 126 empty tupes, both of these pointers aren't pointing to even a single byte of RAM because of course those tuples are zero size, but it is legal to point at them for whatever that's worth...).

I am quite sure that zero length writes are legal for arbitrary pointers, for example Rust considers that trying to store the empty tuple () to the null pointer is a reasonable thing to (unsafely) insist on doing, because it'll just evaporate - the compiler is guaranteed to realise that () is zero bytes wide, and writing zero bytes is not actually a write at all. So I think if there even is a distinction it's a distinction which doesn't make a difference - that "one past the end" pointer can correctly write zero bytes, but so could a "two past the end" pointer.

I am confident the provenance doesn't evaporate when we do this because that's what the (strict provenance API) map_addr trick relies on - we can take a valid pointer, change the address bits in some reversible way and we get an invalid pointer we mustn't dereference, but then later we can reverse the operation on that pointer, and now once again we've got a valid pointer. Flag bits hidden in pointers and some other fun tricks are thus legal in Rust's strict provenance while in C or C++ they're only potentially legalised via a fairly fraught pointer-integer-pointer roundtrip that Rust wanted to avoid. There should be no difference to the resulting machine code after optimisation, but good luck to any tools trying to verify that it's correct in C or C++...

That drop glue statement does seem pretty clear - not sure how I missed that and I agree both that: In practice I'd write a write call to signify my intent and that going by that statement it is legal to use the storing operation instead if you could show that Goose doesn't impl Drop.


Copyright © 2026, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds