-
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: Pointer-to-integer transmutes? #286
Comments
Currently, Miri will perform these implicit ptr-to-int casts in many situations, so just because code is fine under Miri does not mean it is fine under the proposed LLVM semantics. I intend to add a flag that will treat ptr-to-int transmutes as UB. |
So, clarification question: Is this currently UB or will it eventually be UB under a future LLVM? |
I'm not sure that's right - those are the proposed semantics for
AFAICT, they don't intend to change what is UB with this change, just fix bugs and reduce the number of pointers that are considered to escape:
|
That depends on whether you consider LLVM semantics to be defined by the docs or the implementation. ;)
Note that in the problematic line (0) in the code. there is no cast of any kind. So the question here is not the semantics of bitcast, or the semantics of bytecast, but the semantics of an |
I came up with an example that, I think, demonstrates that ptr-to-int transmutes are truly broken. I write these as Rust code for better readability, but you should interpret these as LLVM IR programs. Let's start with this program: // Prepare some pointers to distinct allocated objects.
let mut p = [0];
let paddr = p.as_mut_ptr();
let mut q = [0];
let qaddr = q.as_mut_ptr();
// Set up a bit of storage with 2 ptrs to it: one usize-typed, one ptr-typed.
let mut storage = 0usize;
let storage_usize = &mut storage as *mut usize;
let storage_ptr = storage_usize as *mut *mut i32;
// Now comes the tricky bit.
*storage_ptr = paddr.wrapping_offset(1);
if *storage_usize == qaddr as usize {
let val = qaddr as usize;
*storage_usize = val;
**storage_ptr = 1;
println!("{}", q[0]);
} The one weird bit that is happening here is that we store The program also does a ptr-to-int transmute, namely when it loads If this program does not have UB, it has two possible behaviors:
The first optimization is to exploit the // Prepare some pointers to distinct allocated objects.
let mut p = [0];
let paddr = p.as_mut_ptr();
let mut q = [0];
let qaddr = q.as_mut_ptr();
// Set up a bit of storage with 2 ptrs to it: one usize-typed, one ptr-typed.
let mut storage = 0usize;
let storage_usize = &mut storage as *mut usize;
let storage_ptr = storage_usize as *mut *mut i32;
// Now comes the tricky bit.
*storage_ptr = paddr.wrapping_offset(1);
if *storage_usize == qaddr as usize {
let val = *storage_usize; // this changed
*storage_usize = val;
**storage_ptr = 1;
println!("{}", q[0]);
} The second optimization removes the redundant store to // Prepare some pointers to distinct allocated objects.
let mut p = [0];
let paddr = p.as_mut_ptr();
let mut q = [0];
let qaddr = q.as_mut_ptr();
// Set up a bit of storage with 2 ptrs to it: one usize-typed, one ptr-typed.
let mut storage = 0usize;
let storage_usize = &mut storage as *mut usize;
let storage_ptr = storage_usize as *mut *mut i32;
// Now comes the tricky bit.
*storage_ptr = paddr.wrapping_offset(1);
if *storage_usize == qaddr as usize {
// 2 lines got removed here.
**storage_ptr = 1;
println!("{}", q[0]);
} Next, we replace the // Prepare some pointers to distinct allocated objects.
let mut p = [0];
let paddr = p.as_mut_ptr();
let mut q = [0];
let qaddr = q.as_mut_ptr();
// Set up a bit of storage with 2 ptrs to it: one usize-typed, one ptr-typed.
let mut storage = 0usize;
let storage_usize = &mut storage as *mut usize;
let storage_ptr = storage_usize as *mut *mut i32;
// Now comes the tricky bit.
*storage_ptr = paddr.wrapping_offset(1);
if *storage_usize == qaddr as usize {
*paddr.wrapping_offset(1) = 1; // this changed
println!("{}", q[0]);
} And finally, we exploit that the only 2 writes that happen definitely do not have the provenance of // Prepare some pointers to distinct allocated objects.
let mut p = [0];
let paddr = p.as_mut_ptr();
let mut q = [0];
let qaddr = q.as_mut_ptr();
// Set up a bit of storage with 2 ptrs to it: one usize-typed, one ptr-typed.
let mut storage = 0usize;
let storage_usize = &mut storage as *mut usize;
let storage_ptr = storage_usize as *mut *mut i32;
// Now comes the tricky bit.
*storage_ptr = paddr.wrapping_offset(1);
if *storage_usize == qaddr as usize {
*paddr.wrapping_offset(1) = 1;
println!("{}", 0); // this changed
} This final program now can print "0" if |
Correct me if I'm wrong, but I think that this step is not validated by the current stacked borrows model. Once I hesitate to bring this up again, because it has already been discussed at some length after your provenance blog post, but I think there is a reasonably easy to understand model for pointers here that basically amounts to "pointers don't have provenance". We already have to support "wild" pointers for the case of manufacturing numeric addresses and dereferencing them, and the only thing that is needed to recover the majority of optimizations is to make memory only writable if there is a This problem has been presented as a sort of Gordian knot with no good solution, but it's not like there aren't consistent (and comprehensible!) models for this stuff. I don't think losing this store forwarding optimization is a big deal, partly because it is very contrived, but also because it uses lots of pointer arithmetic and I think that in that case we should encourage people to use references when possible and otherwise do exactly what they wrote and expect the user to take the performance into their own hands. |
No, that is not true. Once
This is incompatible with many optimizations performed by LLVM. I don't think LLVM will switch to a provenance-free model any time soon, and so by extension Rust will not switch to a provenance-free model. In fact I think it is not possible to write a reasonably optimizing compiler for a model where pointers have no provenance. To my knowledge, every single optimizing compiler for languages such as C and C++ has provenance in its memory model. So, the burden of proof here is IMO on folks that don't like provenance: construct at least a reasonable prototype of a compiler that is correct for a provenance-free model. Even register allocation will be hard for such a compiler (since, in first approximation, any write through a pointer might affect any variable that ever had its address taken). If you want to continue this discussion, please open a new thread (or maybe there already is one, I forgot). Questioning the existence of provenance itself is certainly off-topic in this issue.
There aren't -- not if you also want to support a reasonable set of optimizations that modern compilers support. (Of course, there are easy models without provenance, but those are entirely unrealistic as basis for a compiler that wants to produce good assembly.) This is not just my opinion, it is the consensus of all the researchers that I know that work in this field. Again, this is off-topic here. Please don't reply to this part inside this thread; create a new issue and link to it instead.
EDIT: removed my previous reply since I think I misunderstood you. I think you mean the last optimization here, not the one that removes a redundant store. I'm not well-versed in compiler optimization pass jargon. ;) That optimization is a representative example of something compilers do a lot. I have strong doubts that losing it is a realistic option, unless we want to give up on competing with C/C++. |
This comment has been minimized.
This comment has been minimized.
One other possible resolution: if *storage_usize == qaddr as usize {
let val = *storage_usize; // this changed
*storage_usize = val;
**storage_ptr = 1;
println!("{}", q[0]);
} -> if *storage_usize == qaddr as usize {
// 2 lines got replaced with this
erase_provinence(*storage_usize);
**storage_ptr = 1;
println!("{}", q[0]);
} Further transformations are prevented because the compiler can no longer prove that Here (AIUI, this is basically saying that integer variables do not have provenance, but memory locations that happen to store integers may have provenance) |
This comment has been minimized.
This comment has been minimized.
@Diggsey thanks for continuing with the on-topic discussion!
Yes, this is an alternative proposal. However, it means that as far as the IR is concened, there is a write to |
I think "most" is a little strong, at least without more evidence. It still eliminates the store from the resulting program, so the cost is only potentially missed optimizations from later passes. Even then, it's not clear to me that |
To my knowledge, whether or not a piece of code performs a write and whether or not some pointer is written to is very useful information that can have large consequences in the optimizer. Also note that in Rust we have quite a few language or library concepts that make no difference to the machine code ( I guess what I am saying in a round-about way is: this would need serious benchmarking, and it seems unlikely that we can do it with LLVM as our backend.
The later optimization is perfectly fine, it is a standard alias analysis result. The original source program is wrong, IMO. Obviously my example is contrived, because real-world code doing things like this is just way too big to be considered in such detail.^^ For the IR, |
One thing I'd like to clarify: "transmute" is not a thing in C++. The closest equivalent might be So what you actually mean here is that type-punning via a store to memory of a pointer type, followed by a load at an integer type (or vice versa) should be considered UB, and that just happens to be how we define |
Oh, fun. I'll leave that to the C++/clang people to figure out. ;)
Yes. Type-punning raw ptr loads, union field accesses, and |
The closest thing to transmute is C++20's So, I don't think this will cause problems for C++ really, since IIUC we're not saying (Also, as you correctly mention, performing the transmute the way we do in these examples, where the underlying memory is interpreted as a different type, is UB in C++ for other reasons anyway) |
Not always. All memory can be read and written via I don't really understand how user-implemented memcpy works in this model though, assuming it copies with granularity greater than |
Yes, I'm aware of the exceptions around And user-implemented memcpy isn't allowed to copy with greater granularity than char for this reason, which is silly but true, and a good example of a place where Rust does a great deal better than C++ at reflecting semantics that real programs need to be efficient. Anyway, we are well into the weeds at this point and probably off-topic (in a thread that's already had issues with staying on-topic). |
Maybe I'm misunderstanding, but couldn't you simply replace |
Oh, hm, probably. I guess I agree with Ralf then that that's, uh, gonna be a tricky one for the clang folks to work out. |
Whether and how memcpy can be implemented inside C at all is an interesting question -- and a "byte" type is probably part of the answer.
There would be explicit casts between |
The latest/next C++ actually has an explicit |
I'm pretty sure The only places I can find where https://github.com/cplusplus/draft mentions it that aren't as part of a list of the other char types are where it describes which header it's found in and such. So I think it's not really the same as the proposed llvm bytes type in any meaningful way, unless |
Yes, those are also LLVM |
My main problem with stripping provenance implicitly is that it breaks the entire framework I have set up here: in that framework, if a list of bytes If we don't have that symmetry, we double the size of that file by having to define "encode" and "decode" separately for each type. That also means more work if we have to ensure any properties that relate these operations. Note that having the symmetry while doing implicit provenance stripping is not an option: that means encoding an integer could synthesize arbitrary provenance, which violates the most fundamental property of provenance -- that it is "private" and can be "owned" by a function. This property is the reason why provenance exists in the first place (from a program logic / reasoning perspective), it is not negotiable.
I also just now wondered if we can even have that property. Specifically, the property I was thinking of is: given an Abstract Machine state, if we alter that state by changing a single byte in memory to "increase" its provenance, then all executions that are possible from the altered machine state were also possible from the original machine state. This property certainly holds (has to hold!) when replacing an "uninit" byte by an arbitrary byte; do we also have it for increasing provenance? I think the answer is no. The key question is how we define the "decode" operation for pointers when not all bytes have the same provenance:
If we truly don't have this property, then I am feeling strongly that we can not implicitly strip provenance when decoding integers. The combination of not having the property and implicit provenance stripping means that we cannot remove |
@digama0 proposed a different "decode" operation that uses the meet of all provenances (the "least common provenance") when decoding a pointer and the bytes do not all have the same provenance. (Basically: if any byte's provenance is Algebraically this means we have to require a "meet" to exist on So maybe we have to choose between "encode and decode form an adjoint" and "provenance monotonicity". |
Turns out there is a way to also get an adjoint property with implicit provenance stripping. So the algebraic property is not at risk. What remains is the question whether having provenance stripped implicitly is a good idea. (Also see this Zulip discussion.)
I am slightly worried we will be confusing people if we tell them these transmutes are okay but the resulting pointer is garbage. Though maybe I am overestimating the difference between that, and telling them the transmutes are UB -- many will also find that confusing. |
If I had to pick, "the transmute is UB" is probably easier to teach. |
This could be a good case to apply https://gankra.github.io/blah/tower-of-weakenings/ though, since it sounds a lot like the difference between "a painfully strict and simple model that you can teach and check" and "messier to allow for Useful Crimes". |
Yeah, I had the same thought -- we can tell people ptr2int transmutes are "tricky, don't do them", and then if people care about the fine-print we can explain the details of what exactly is and is not allowed. In this case we don't know the Useful Crimes yet, but who knows what people will come up with. |
After all these discussions, I think I now agree with those who said that ptr2int transmutation should strip provenance, i.e., it should be equivalent to |
implement ptr.addr() via transmute As per the discussion in rust-lang/unsafe-code-guidelines#286, the semantics for ptr-to-int transmutes that we are going with for now is to make them strip provenance without exposing it. That's exactly what `ptr.addr()` does! So we can implement `ptr.addr()` via `transmute`. This also means that once rust-lang#97684 lands, Miri can distinguish `ptr.addr()` from `ptr.expose_addr()`, and the following code will correctly be called out as having UB (if permissive provenance mode is enabled, which will become the default once the [implementation is complete](rust-lang/miri#2133)): ```rust fn main() { let x: i32 = 3; let x_ptr = &x as *const i32; let x_usize: usize = x_ptr.addr(); // Cast back an address that did *not* get exposed. let ptr = std::ptr::from_exposed_addr::<i32>(x_usize); assert_eq!(unsafe { *ptr }, 3); //~ ERROR Undefined Behavior: dereferencing pointer failed } ``` This completes the Miri implementation of the new distinctions introduced by strict provenance. :) Cc `@Gankra` -- for now I left in your `FIXME(strict_provenance_magic)` saying these should be intrinsics, but I do not necessarily agree that they should be. Or if we have an intrinsic, I think it should behave exactly like the `transmute` does, which makes one wonder why the intrinsic should be needed.
… r=oli-obk interpret: better control over whether we read data with provenance The resolution in rust-lang/unsafe-code-guidelines#286 seems to be that when we load data at integer type, we implicitly strip provenance. So let's implement that in Miri at least for scalar loads. This makes use of the fact that `Scalar` layouts distinguish pointer-sized integers and pointers -- so I was expecting some wild bugs where layouts set this incorrectly, but so far that does not seem to happen. This does not entirely implement the solution to rust-lang/unsafe-code-guidelines#286; we still do the wrong thing for integers in larger types: we will `copy_op` them and then do validation, and validation will complain about the provenance. To fix that we need mutating validation; validation needs to strip the provenance rather than complaining about it. This is a larger undertaking (but will also help resolve rust-lang/miri#845 since we can reset padding to `Uninit`). The reason this is useful is that we can now implement `addr` as a `transmute` from a pointer to an integer, and actually get the desired behavior of stripping provenance without exposing it!
…=oli-obk interpret: better control over whether we read data with provenance The resolution in rust-lang/unsafe-code-guidelines#286 seems to be that when we load data at integer type, we implicitly strip provenance. So let's implement that in Miri at least for scalar loads. This makes use of the fact that `Scalar` layouts distinguish pointer-sized integers and pointers -- so I was expecting some wild bugs where layouts set this incorrectly, but so far that does not seem to happen. This does not entirely implement the solution to rust-lang/unsafe-code-guidelines#286; we still do the wrong thing for integers in larger types: we will `copy_op` them and then do validation, and validation will complain about the provenance. To fix that we need mutating validation; validation needs to strip the provenance rather than complaining about it. This is a larger undertaking (but will also help resolve rust-lang/miri#845 since we can reset padding to `Uninit`). The reason this is useful is that we can now implement `addr` as a `transmute` from a pointer to an integer, and actually get the desired behavior of stripping provenance without exposing it!
So, is this pretty much settled as "pointers->integers is not expected to be UB, but you should assume it strips provenance", or is it more up in the air than that? |
I have personally pretty much settled on that, yes. It's also what Miri implements now, modulo rust-lang/miri#2182. I'd love to make this "official", but I am not sure what the right process is for that. Probably an RFC? Or just FCP'ing the strict provenance APIs into stabilization? |
caution against ptr-to-int transmutes I don't know how strong of a statement we want to make here, but I am very concerned that the current docs could be interpreted as saying that ptr-to-int transmutes are just as okay as transmuting `*mut T` into an `&mut T`. Examples [like this](rust-lang/unsafe-code-guidelines#286 (comment)) show that ptr-to-int transmutes are deeply suspicious -- they are either UB, or they don't round-trip properly, or we have to basically say that `transmute` will actively look for pointers and do all the things a ptr-to-int cast does (which includes a global side-effect of marking the pointed-to allocation as 'exposed'). Another alternative might be to simply not talk about them... but we *do* want people to use casts rather than transmutes for this. Cc `@rust-lang/lang`
implement ptr.addr() via transmute As per the discussion in rust-lang/unsafe-code-guidelines#286, the semantics for ptr-to-int transmutes that we are going with for now is to make them strip provenance without exposing it. That's exactly what `ptr.addr()` does! So we can implement `ptr.addr()` via `transmute`. This also means that once rust-lang/rust#97684 lands, Miri can distinguish `ptr.addr()` from `ptr.expose_addr()`, and the following code will correctly be called out as having UB (if permissive provenance mode is enabled, which will become the default once the [implementation is complete](rust-lang/miri#2133)): ```rust fn main() { let x: i32 = 3; let x_ptr = &x as *const i32; let x_usize: usize = x_ptr.addr(); // Cast back an address that did *not* get exposed. let ptr = std::ptr::from_exposed_addr::<i32>(x_usize); assert_eq!(unsafe { *ptr }, 3); //~ ERROR Undefined Behavior: dereferencing pointer failed } ``` This completes the Miri implementation of the new distinctions introduced by strict provenance. :) Cc `@Gankra` -- for now I left in your `FIXME(strict_provenance_magic)` saying these should be intrinsics, but I do not necessarily agree that they should be. Or if we have an intrinsic, I think it should behave exactly like the `transmute` does, which makes one wonder why the intrinsic should be needed.
Transmuting pointers to integers (i.e., not going through the regular cast) is a problem. This is demonstrated by the following silly example:
Imagine executing this code on the Abstract Machine, taking into account that pointers have provenance, i.e., a ptr-to-int conversion loses information. Now what happens at point (0)? Here we read the data stored in
storage
at typeusize
. That data however is the ptrptr
, i.e., it has provenance. What should happen with that provenance at (0)?storage
acts like an implicit ptr-to-int cast. The problem with this approach is that we cannot remove the redundant write at (1): the value inval
is different from what is stored instorage
, sinceval
has no provenance but theptr
stored instorage
does! This is basically another version of https://bugs.llvm.org/show_bug.cgi?id=34548: ptr-to-int casts are not NOPs, and a ptr-int-ptr roundtrip cannot be optimized away. If a load, like at (0), can perform a ptr-to-int cast, now the same concerns apply here.val
having typeusize
and also having provenance, which is a big problem: the compiler might decide, at program point (2), toreturn val
instead ofreturn cmp
(based on the fact thatval == cmp
), but ifval
could have provenance then this transformation is wrong! This is basically the isue at the heart of my blog post on provenance:==
ignores provenance, so just because two values are equal according to==
does not mean they can be used interchangeably in all circumstances.poison
-- effectively declaring ptr-to-int transmutes as UB.The last option is what is being proposed to LLVM, along with a new "byte" type such that loading at type
bN
would preserve provenance, but loading at typeiN
would turn bytes with provenance intopoison
. On the flipside, no arithmetic or logical operations are possible onbN
; that type represents "opaque bytes" with the only possible operations being load and store (and explicit casts to remove any provenance that might exist). This leads to a consistent model in which both redundant store elimination and GVN substitution on integer types (the optimizations mentioned above) are possible. I don't know any other way to resolve the contradiction that otherwise arises from doing both of these optimizations. However, the LLVM discussion is still in its early stages, and there were already a lot of responses that I have not read in detail yet. If this ends up being accepted, we on the Rust side will have to figure out if and how we can make use of the new "byte" type and its explicit casts (to pointers or integers).This thread is about discussing how we need to restrict ptr-to-int transmutes when pointers have provenance but integers do not. See #287 for a discussion with the goal of avoiding provenance in the first place.
The text was updated successfully, but these errors were encountered: