-
Notifications
You must be signed in to change notification settings - Fork 59
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: zero-sized dangling accesses/inbounds-offsets? #93
Comments
I personally prefer that accesses of a zero sized type are always valid under raw pointers, and for references to only need alignment. |
We don't make a difference between raw pointer and reference accesses in terms of alignment for non-zero-sized accesses, why would we for zero-sized accesses? |
From the LLVM side, it seems rather unambigious to me that the current GEPi specification does not make an exception for offset 0 and so any GEPi on a dangling pointer results in poison. I don't really see a way to change that without hampering analyses based on GEPi: for sanity such a rule should apply to runtime offsets as well as compile time offsets, but then any analysis that wants to draw conclusions from the presence of a GEPi would have to prove that the offset is nonzero at runtime. One could possibly argue that any pointer is inbounds for ZSTs since it's one past the end of a zero-sized allocation, but I do not believe such a concept exists in LLVM today (and it's unclear to me whether that would be desirable). How about a different solution? For ZSTs, projections don't do any address calculation anyway, so we could just emit a bitcast to change the pointee type. That shouldn't lose any AA precision and avoids the implications of the GEPi. |
I just realized we recently had a related discussion in rust-lang/rust#54857.
Just to be clear, does "dangling pointer" include Cc @eddyb who argued previously that we can rely on such "zero-sized allocations".
This still leaves empty slices, where the offset is only known at run-time. |
Interesting question, I don't know. More precisely the LangRef says the result is poison when "the base pointer is not an in bounds address of an allocated object", so this too runs into the question of zero sized allocations. At the same time, I would be wary of spec-lawyering too much in this context.
Ugh, yeah, what a pain. |
I brought this up on the LLVM-dev list and had a bit of an exchange with one developer over there. They concluded that
|
We already do this! It's needed because there are more things with offset (ugh, why does that kind of linking not work cross-crate, @github?!) // Unions and newtypes only use an offset of 0.
let llval = if offset.bytes() == 0 {
self.llval
} else /*...*/;
PlaceRef {
// HACK(eddyb) have to bitcast pointers until LLVM removes pointee types.
llval: bx.pointercast(llval, bx.cx().type_ptr_to(bx.cx().backend_type(field))), |
We can't always do this for all 0-offsets though, like for |
@RalfJung Right, unless |
Sure. However, |
Would that means it is safe to conjure any slice length of a ZST out of thin air? Would it make this code UB-free? It seems that this has at least some impact on privacy as it creates some references to fn arbitrary<'a, T>(length: usize) -> &'a [T] {
assert!(core::mem::size_of::<T>() == 0);
unsafe {
core::slice::from_raw_parts(core::ptr::NonNull::dangling().as_ptr(), length)
}
} |
Just because something does not immediately cause UB doesn't mean that it is safe in the sense that it composes with other code. A lot of code (even purely safe code, depending on whether we e.g. consider |
@HeroicKatora Beyond what @rkruppe said, see this blog post that explains the difference between the kind of "wrong code" that causes immediate UB, vs the kind of "wrong code" that violates a high-level semantic contract and is therefore guilty of making other code have UB. This even older post is also relevant. |
So yes, it is memory safe in that it does not violate the validity invariants of a |
|
Ah, good point. I'm asking due to the fn arbitrary_blowup<'a, T>(slice: &'a [T], length: usize) -> &'a [T] {
assert!(core::mem::size_of::<T>() == 0);
assert!(!slice.is_empty());
unsafe {
core::slice::from_raw_parts(slice.as_ptr(), length);
}
} In the spirit of this code, but maybe part of another issue, is |
No. It is inhabited by
However, as discussed above, |
Why would |
Let me put it the other way: There is no such thing as "creating an instance" of a ZST, but increasing the size of a ZST slice from 1 to 2 is effectively "duplicating" the first element -- aka doing a copy. |
Now I'm confused. All references obtained from the slice are indistinguishable. Non-mutable references are |
@HeroicKatora A ZST could have a Edit: Adding a |
It doesn't lead to any new Edit: sorry, this was a mess at first. I deleted the comment and added a new for anyone following by mail. |
I also agree that I don't understand the need for If this were an array then yes you'd need to not make them up out of nowhere, but since this is a shared slice then adding more length to the slice doesn't do anything because when the slice drops it doesn't cause the elements to drop. Edit: Clarification, I mean in the case of concat for shared slices. Obviously you can't go making up any length that you want for any type that you want, but if you're concatting slices then the only way that you'd get a non-zero length input for one of the slices being concat is if someone gave you an inhabited type or already did UB themselves. |
Hm I see, I think you are right. A shared slice is just many shared references and having one more makes no difference. I should try to prove that in lambda-rust one day. For But anyway this is all off-topic for a discussion that is solely about offsetting. I should probably tell GItHub to hide all the posts here... @HeroicKatora next time you have a new question triggered by discussion inside a thread that is not directly related to the thread's topic, please open a new issue! |
I’m not sure I follow, arbitrary blowup gets a reference to a slice, and
increases the size of the slice, returning a reference to it. AFAICT this
creates a reference to values that do not exist.
If the reference validity is not transitive, then it doesn’t matter whether
what the reference points to exists or not. But I’d reference validity is
transitive, then those new ZSTs at the end of the slice are required to
exist somewhere. I don’t see how creating a reference to a ZST creates the
ZST itself.
|
Forked to it's own issue: #168 |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Ah yes, I saw the comment by email and replied by email, so I didn't see the edit.
That's what the "This is what I would like" is there for, along with the 3rd property. I'd like for |
If we allow |
This seems like it might conflict with some of the more interesting inferences I'd like to make, though I'm trying to figure out what those interfences might be. An obvious one would be pruning branches that lead to such an access to a deallocated pointer. one thing I'm considering is replacing it with a fixed sized load in machine code emission when well-aligned, though I'm currently having a fun time justifying that on a ZST access. Along with actually using a |
Hmm... actually, potential idea for why deallocated pointers especially shouldn't work. Sort of a contrived example, but: pub fn foo(x: &AtomicU32,bytelen: usize){
assert_le!(bytelen,4);
x.fetch_sub(1,Ordering::Acquire);
let y =unsafe{core::slice::from_raw_parts(x as *const AtomicU32 as *const u32 as *const u8,bytelen)};
for b in y{//...}
} Being able to turn that into For |
IIUC, in that example |
That example certainly is extremely contrived, since you're doing mixed atomic and nonatomic reads. I believe your point here is that if ZST access to deallocated is UB, that (I should also note that if you're optimizing on an IR where reading deallocated is not immediate UB but rather returns Rewriting the example a little to avoid distractions: pub unsafe fn foo(
ptr: *const u32,
len: usize,
) {
assert!(len <= 4);
// ptr is dereferencable here for `u32`
*ptr;
// some other code runs
extern_fn();
// ptr is dereferencable here for `()`
*(ptr as *const ());
// iterate over the first `len` bytes
let slice = slice::from_raw_parts(ptr as *const u8, len);
for &byte in slice { /* ... */ }
} Can that be transformed to (the moral equivalent of) pub unsafe fn foo(
ptr: *const u32,
len: usize,
) {
assert!(len <= 4);
// some other code runs
extern_fn();
// read as `u32` (4 bytes)
let arr = (*ptr).to_ne_bytes();
// then iterate over the first `len` bytes
let slice = &arr[..len];
for &byte in slice { /* ... */ }
} which unconditionally does a The I can illustrate this transformation introducing new UB without relying on the deallocation of unsafe fn extern_fn() {
let ptr: *const u32 = get_from_collusion();
let bptr = (ptr as *const u8 as *mut u8).add(3);
thread::spawn(|| *bptr = 0);
} Now the unconditional (The same thing about using an IR where a racing read results in Just in general, introducing any access through |
Note that by The optimization in question relies on the potentially-zst access extending the dereferenceable, since AFAIK it's impossible to shrink reference provenance, only invalidate it completely. I'm actually thinking that my
Such an optimization might occur in MCE, rather than on IR. xlang would also permit lifting to |
(As a general note, I'm writing transformed code in Rust to avoid adding to confusion - assume that the transformations are taking place on an IR like xlang, which, among other things, lifts data race UB to |
I don't think we need a data race; |
I'm fairly certain that shouldn't be a problem with xlang's model (again, assume I'm doing rewrites on xlang, not on rust source or a MIR equivalent). An example of how it might introduce the rust-level UB might be useful in assesing whether or not it would definitively. If such a function does exist, doing the transform in MCE is still valid. |
(I almost want to say that reads/writes to reachable bytes that aren't reachable for that purpose does |
As a note, thinking about how this optimizes: On w65 this can nominally save 4 cycles per 2 bytes getting speculated (at the potential cost of 6 cycles for a Edit: This came up on the Rust Community Server, so I'd like to clarify - I don't think that these cycle savings on w65 are the only performance benefit I can derive from this, but as you can imagine, it's alot harder to give concrete numbers on a modern CPU even if you do have the optimization implemented, let alone one on the "To implement" list. |
About the fact this came up on the community server, I recenrtly heard arguments that part of the issue is that it's really weird to allow dangling ZST accesses, but not deallocated ZST accesses. While that's true, I think the real issue is a matter of perspective. The apparent inconsistency is that "it's ok to read ZSTs from dangling pointers, but not if deallocated". However, consider this alternative formulation:
Under this model, the rules are simple: Nothing, not even a ZST, can be read from a dangling pointer. Just like nothing, not even a ZST, can be read from a deallocated pointer. This interpretation also seems to be supported by the core::ptr docs: https://doc.rust-lang.org/std/ptr/index.html#safety
Emphasis added. My reading of this is that the operation of casting a pointeger from a nonzero integer performs a zero-sized allocation, and I think it would be fine to generalize this idea to all pointers, not just pointers from "magic integers". To be entirely fair, this model would cause * Performing such a read can still be UB for other reasons, such as violation of validity invariants. It's just not itself a cause of UB. |
I don't think this is an option, because it means "creating a pointer" is not a pure operation. In particular, you put For the "provenance monotonicity" property to work, you need the possible provenances to have a lattice structure, meaning that there needs to be a bottom provenance which has no more permissions than a deallocated pointer, which means we need a second "none"-like provenance, say
ZST types are not special, but accesses are effectively a for loop over all the loaded memory locations - a permissions error on any of the bytes will cause UB - and so a zero-byte access should be an empty loop and hence a no-op. There are no borrow stacks to consult in the first place as these permissions are stored per-byte. |
There was an agreement in which we agreed on allowing this for offsets - the accesses question was not covered |
Use GEP inbounds for ZST and DST field offsets ZST field offsets have been non-`inbounds` since I made [this old layout change](https://github.com/rust-lang/rust/pull/73453/files#diff-160634de1c336f2cf325ff95b312777326f1ab29fec9b9b21d5ee9aae215ecf5). Before that, they would have been `inbounds` due to using `struct_gep`. Using `inbounds` for ZSTs likely doesn't matter for performance, but I'd like to remove the special case. DST field offsets have been non-`inbounds` since the alignment-aware DST field offset computation was first [implemented](erikdesjardins@a2557d4#diff-04fd352da30ca186fe0bb71cc81a503d1eb8a02ca17a3769e1b95981cd20964aR1188) in 1.6 (back then `GEPi()` would be used for `inbounds`), but I don't think there was any reason for it. Split out from rust-lang#121577 / rust-lang#121665. r? `@oli-obk` cc `@RalfJung` -- is there some weird situation where field offsets can't be `inbounds`? Note that it's fine for `inbounds` offsets to be one-past-the-end, so it's okay even if there's a ZST as the last field in the layout: > The base pointer has an in bounds address of an allocated object, which means that it points into an allocated object, or to its end. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction) For rust-lang/unsafe-code-guidelines#93, zero-offset GEP is (now) always `inbounds`: > Note that getelementptr with all-zero indices is always considered to be inbounds, even if the base pointer does not point to an allocated object. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction)
Use GEP inbounds for ZST and DST field offsets ZST field offsets have been non-`inbounds` since I made [this old layout change](https://github.com/rust-lang/rust/pull/73453/files#diff-160634de1c336f2cf325ff95b312777326f1ab29fec9b9b21d5ee9aae215ecf5). Before that, they would have been `inbounds` due to using `struct_gep`. Using `inbounds` for ZSTs likely doesn't matter for performance, but I'd like to remove the special case. DST field offsets have been non-`inbounds` since the alignment-aware DST field offset computation was first [implemented](erikdesjardins@a2557d4#diff-04fd352da30ca186fe0bb71cc81a503d1eb8a02ca17a3769e1b95981cd20964aR1188) in 1.6 (back then `GEPi()` would be used for `inbounds`), but I don't think there was any reason for it. Split out from rust-lang#121577 / rust-lang#121665. r? `@oli-obk` cc `@RalfJung` -- is there some weird situation where field offsets can't be `inbounds`? Note that it's fine for `inbounds` offsets to be one-past-the-end, so it's okay even if there's a ZST as the last field in the layout: > The base pointer has an in bounds address of an allocated object, which means that it points into an allocated object, or to its end. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction) For rust-lang/unsafe-code-guidelines#93, zero-offset GEP is (now) always `inbounds`: > Note that getelementptr with all-zero indices is always considered to be inbounds, even if the base pointer does not point to an allocated object. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction)
Use GEP inbounds for ZST and DST field offsets ZST field offsets have been non-`inbounds` since I made [this old layout change](https://github.com/rust-lang/rust/pull/73453/files#diff-160634de1c336f2cf325ff95b312777326f1ab29fec9b9b21d5ee9aae215ecf5). Before that, they would have been `inbounds` due to using `struct_gep`. Using `inbounds` for ZSTs likely doesn't matter for performance, but I'd like to remove the special case. DST field offsets have been non-`inbounds` since the alignment-aware DST field offset computation was first [implemented](erikdesjardins/rust@a2557d4#diff-04fd352da30ca186fe0bb71cc81a503d1eb8a02ca17a3769e1b95981cd20964aR1188) in 1.6 (back then `GEPi()` would be used for `inbounds`), but I don't think there was any reason for it. Split out from #121577 / #121665. r? `@oli-obk` cc `@RalfJung` -- is there some weird situation where field offsets can't be `inbounds`? Note that it's fine for `inbounds` offsets to be one-past-the-end, so it's okay even if there's a ZST as the last field in the layout: > The base pointer has an in bounds address of an allocated object, which means that it points into an allocated object, or to its end. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction) For rust-lang/unsafe-code-guidelines#93, zero-offset GEP is (now) always `inbounds`: > Note that getelementptr with all-zero indices is always considered to be inbounds, even if the base pointer does not point to an allocated object. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction)
Use GEP inbounds for ZST and DST field offsets ZST field offsets have been non-`inbounds` since I made [this old layout change](https://github.com/rust-lang/rust/pull/73453/files#diff-160634de1c336f2cf325ff95b312777326f1ab29fec9b9b21d5ee9aae215ecf5). Before that, they would have been `inbounds` due to using `struct_gep`. Using `inbounds` for ZSTs likely doesn't matter for performance, but I'd like to remove the special case. DST field offsets have been non-`inbounds` since the alignment-aware DST field offset computation was first [implemented](erikdesjardins/rust@a2557d4#diff-04fd352da30ca186fe0bb71cc81a503d1eb8a02ca17a3769e1b95981cd20964aR1188) in 1.6 (back then `GEPi()` would be used for `inbounds`), but I don't think there was any reason for it. Split out from #121577 / #121665. r? `@oli-obk` cc `@RalfJung` -- is there some weird situation where field offsets can't be `inbounds`? Note that it's fine for `inbounds` offsets to be one-past-the-end, so it's okay even if there's a ZST as the last field in the layout: > The base pointer has an in bounds address of an allocated object, which means that it points into an allocated object, or to its end. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction) For rust-lang/unsafe-code-guidelines#93, zero-offset GEP is (now) always `inbounds`: > Note that getelementptr with all-zero indices is always considered to be inbounds, even if the base pointer does not point to an allocated object. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction)
Use GEP inbounds for ZST and DST field offsets ZST field offsets have been non-`inbounds` since I made [this old layout change](https://github.com/rust-lang/rust/pull/73453/files#diff-160634de1c336f2cf325ff95b312777326f1ab29fec9b9b21d5ee9aae215ecf5). Before that, they would have been `inbounds` due to using `struct_gep`. Using `inbounds` for ZSTs likely doesn't matter for performance, but I'd like to remove the special case. DST field offsets have been non-`inbounds` since the alignment-aware DST field offset computation was first [implemented](erikdesjardins/rust@a2557d4#diff-04fd352da30ca186fe0bb71cc81a503d1eb8a02ca17a3769e1b95981cd20964aR1188) in 1.6 (back then `GEPi()` would be used for `inbounds`), but I don't think there was any reason for it. Split out from #121577 / #121665. r? `@oli-obk` cc `@RalfJung` -- is there some weird situation where field offsets can't be `inbounds`? Note that it's fine for `inbounds` offsets to be one-past-the-end, so it's okay even if there's a ZST as the last field in the layout: > The base pointer has an in bounds address of an allocated object, which means that it points into an allocated object, or to its end. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction) For rust-lang/unsafe-code-guidelines#93, zero-offset GEP is (now) always `inbounds`: > Note that getelementptr with all-zero indices is always considered to be inbounds, even if the base pointer does not point to an allocated object. [(link)](https://llvm.org/docs/LangRef.html#getelementptr-instruction)
Can we close this issue based on #472 or am I missing something? |
Yes I think this is resolved. :) |
Is the following code UB or not?
On the one hand,
x
is dangling. On the other hand, doing the same thing withlet x: *mut ((),) = 1usize as *mut ((),)
would definitely be allowed. Does it make sense to treat dangling pointers as "more dangerous" than integer pointers?AFAIK the actual accesses do not get translated to LLVM IR, so we are not constrained by LLVM. But we do emit a
getelementptr inbounds
, and the rules for that with offset 0 are rather unclear, I would say. @rkruppe do you know anything about this?Sub-threads / points:
The text was updated successfully, but these errors were encountered: