You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Stacked Borrows: raw pointer usable only for T too strict? #134
Currently, the following is illegal according to Stacked Borrows:
let val = [1u8,2];let ptr = &val[0]as*constu8;let _val = unsafe{*ptr.add(1)};
The problem is that the cast to *const u8 creates a raw pointer that may only be used for the u8 it points to, not anything else. The most common case is to do &slice[0] as *const _ instead of slice.as_ptr().
Maybe this is too restrictive and raw pointers should be allowed to access their "surroundings"? I am not sure what exactly that would look like though. It would probably require having the raw pointer fully inherit all permissions from the reference it is created from.
I'll use this issue to collect such cases.
Reacted by gnzlbg, comex, Dylan MacKenzie, oilaba, Troy Hinckley, Tyler Mandry, Smitty, Diana and A248
ptr may only be used to access the T part of the RcBox<T>, but if later used with from_raw it is used for the entire Rc. Fixing this is not even possible without rust-lang/rfcs#2582.
This also came up in Gilnaa/memoffset#21, where @Amanieu proposed an container_of! macro that computes the address of a struct given the address of one of its fields.
structPair{f1:u16,f2:u16};let p = Pair{f1:2,f2:3};let c = container_of!(&p.f1,Pair, f1);let _val = c.f2;
arises when you imagine splitting it across several functions:
structPair{f1:u16,f2:u16};let p = Pair{f1:2,f2:3};foo(&p.f1);
p.f2 = 4;
We want the compiler to be able to move the assignment to f2 up across the call to foo. But if foo is allowed to use container_of! and then read f2, that is no longer possible.
So, I think there is a real conflict here between being able to bound the effects of a call like foo(&p.f1), and allowing container_of!.
@Lokathor yes. Those methods do the right thing. They cast the wide reference to a wide raw pointer, and only then go to thin -- so the ref-to-raw cast has the right "span" in memory.
rust-lang/rust#64980 gives a minor reason to maintain the status quo here. It includes a test-case for a dataflow analysis that computes whether a given Local is mutable through a reference. This test would show the analysis to be unsound if it were legal to offset a reference to one field into a pointer to another, disjoint field.
It is possible to relax the analysis so that it remains sound if this behavior became defined (see rust-lang/rust#65030).
A common violation of this rule is FFI structs that end with a flexible array. For example:
#[repr(C)]structFoo{// other fieldslength:u32,data:[u8;0],}// later in codelet foo:*mutFoo = /* obtained somehow */;let data = slice::from_raw_parts((*foo).data.as_ptr(),(*foo).length);
This pattern is used in quite a few crates that deal with windows api, and some others that don't!
That is an interesting special case. It is actually less problematic than the other cases because there is no Rust reference pointing to that "extra" memory (unlike the example in the OP where val still "owns" that extra memory). I am not sure however how to incorporate that into Stacked Borrows...
Tree Borrows solves this issue by not doing any retagging on raw pointers, and just giving them the same permission as the reference they are created from. However doing that without two-phase borrows is tricky; here's some discussion.
Yes, it applies to "ref-to-ptr coërcions" as well. I think the "cast" terminology is used here kind of indiscriminately to cover both explicitas ... casts, and implicit casts, such as the ones stemming from "coërcions" like with let ptr: *const u8 = &val[0]; or identity::<*const u8>(&val[0]). The terminology in the book about casts vs. coërcions is a bit muddy, to be honest.
Yes indeed, we often view coercions as just automatically inserted casts. Opsem discussions generally are happening on a level where all these implicit operations are made explicit. I can see how that can make the terminology confusing though.
The rules were very confusing at first, and I kept running into SB violations, but I think I understand better now. Let me describe what I did for others who are interested -- and if I'm wrong, one of the seniors can correct me.
Originally I was taking a reference to the array element and coercing it to a pointer, but that violates SB. Fortunately, I was able to adapt @saethlin 's example code and look at the linked issues, and derive a solution that made MIRI happy. The trick is to take the pointer from the big allocation, then use pointer arithmetic to arrive at the field's pointer. This way, the computed field pointer inherits the access from the big struct's pointer (which grants access to the whole allocation).
Importantly, my initial attempts to make this work kept failing. I had an Arc pointing to an allocated slice DST, and I wanted to take pointers to array elements and "rewind" them to the start of the allocation later. However, my code kept taking references that I wasn't aware of. The final solution (to get a correctly permissive pointer) looked like this:
// SharedStorage is a slice DST, with [SlotState] as the tail// Passing the Arc and using Arc::as_ptr was the last hurdle I solvedfnget_slot_state_ptr(shared_storage:&Arc<SharedStorage>,slot_idx:usize) -> *constSlotState{unsafe{// SAFETY// We move the pointer to where the slot is locatedlet storage_ptr = Arc::as_ptr(shared_storage);let ptr = storage_ptr.byte_add(SharedStorage::HEADER_BYTE_OFFSET);let ptr = ptr as*constSlotState;let ptr = ptr.add(slot_idx);
ptr
}}
Activity
RalfJung commentedon May 28, 2019
This is also a problem in
Rc::into_raw/Rc::from_raw:ptrmay only be used to access theTpart of theRcBox<T>, but if later used withfrom_rawit is used for the entireRc. Fixing this is not even possible without rust-lang/rfcs#2582.Auto merge of #80 - RalfJung:raw, r=Amanieu
RalfJung commentedon Aug 16, 2019
This also came up in Gilnaa/memoffset#21, where @Amanieu proposed an
container_of!macro that computes the address of a struct given the address of one of its fields.RalfJung commentedon Aug 17, 2019
@Amanieu the problem with code like this
arises when you imagine splitting it across several functions:
We want the compiler to be able to move the assignment to
f2up across the call tofoo. But iffoois allowed to usecontainer_of!and then readf2, that is no longer possible.So, I think there is a real conflict here between being able to bound the effects of a call like
foo(&p.f1), and allowingcontainer_of!.Lokathor commentedon Aug 19, 2019
How does this work with
[T]::as_ptr? Does that pointer let you use "the whole slice" when offsetting?RalfJung commentedon Aug 20, 2019
@Lokathor yes. Those methods do the right thing. They cast the wide reference to a wide raw pointer, and only then go to thin -- so the ref-to-raw cast has the right "span" in memory.
ecstatic-morse commentedon Oct 5, 2019
rust-lang/rust#64980 gives a minor reason to maintain the status quo here. It includes a test-case for a dataflow analysis that computes whether a given
Localis mutable through a reference. This test would show the analysis to be unsound if it were legal to offset a reference to one field into a pointer to another, disjoint field.It is possible to relax the analysis so that it remains sound if this behavior became defined (see rust-lang/rust#65030).
RalfJung commentedon Dec 22, 2019
See this discussion for a related example.
comex commentedon Dec 23, 2019
Another example caught by miri. The offending code:
Rollup merge of rust-lang#70479 - RalfJung:win-env, r=Mark-Simulacrum
retep998 commentedon May 12, 2020
A common violation of this rule is FFI structs that end with a flexible array. For example:
This pattern is used in quite a few crates that deal with windows api, and some others that don't!
A few examples after a quick search:
https://github.com/kavorite/imgclip/blob/f3889e9da99fde01bd688e5ee2a18d89c823dee6/src/dib.rs#L53
https://github.com/rust-lang/backtrace-rs/blob/16682c76eb25df517e2cc220e56baf4f8a616f72/src/symbolize/dbghelp.rs#L165
https://github.com/klenin/spawner2/blob/0d461fd59e09fbcf9e863acf9a5db441da58b7a2/spawner/sys/windows/helpers.rs#L450
https://github.com/benfred/remoteprocess/blob/cdbf4aa23f48b48f949da3dadfc5878ab6e94f53/src/windows/symbolication.rs#L76
https://github.com/notify-rs/notify/blob/eed64ac9088ec1aab5c4710ef7232f0fbee49e0a/src/windows.rs#L328
https://github.com/CasualX/pelite/blob/29c4cac31c2ff4a7fd3d7953e9af8ea97c55423b/src/pe64/exception.rs#L180
https://github.com/snuk182/nuklear-rust/blob/1fb270a527ff048bb7e38dd88553c98987af233c/src/lib.rs#L5678
https://github.com/diwic/reffers-rs/blob/e6bdacafb96dae5dfa95a0830dda18fd64928691/src/rc.rs#L320
https://github.com/szymonwieloch/rust-dlopen/blob/26b70292744db1052403378d4a01075b6171e3d1/src/raw/windows.rs#L213
https://github.com/PyO3/pyo3/blob/956ed524122d27597888c28199053e6ba3f5289d/src/types/tuple.rs#L85
https://github.com/gluon-lang/gluon/blob/7b4cb090037d5ddf62dc35fde0b316689e5e4de3/vm/src/array.rs#L103
https://github.com/redsift/redbpf/blob/bea9eff8d2eedf0a57e90c8b7191a465cffdc56f/redbpf/src/load/map_io.rs#L70
https://github.com/servo/html5ever/blob/304c155bce19f956f0641cce3a1e19bc3b7bdaa1/zbuf/heap_data.rs#L178
https://github.com/arcnmx/ddcutil-rs/blob/3a631cdc92fcffcfc6e97fa29b4ed607fafc52d8/sys/src/lib.rs#L226
RalfJung commentedon May 13, 2020
That is an interesting special case. It is actually less problematic than the other cases because there is no Rust reference pointing to that "extra" memory (unlike the example in the OP where
valstill "owns" that extra memory). I am not sure however how to incorporate that into Stacked Borrows...26 remaining items
Dependabot cleanup and fixup CI (#6)
RalfJung commentedon Apr 1, 2024
Tree Borrows solves this issue by not doing any retagging on raw pointers, and just giving them the same permission as the reference they are created from. However doing that without two-phase borrows is tricky; here's some discussion.
briansmith commentedon May 23, 2024
The problem is not restricted to casts, but also affects coercions, right? Consider the equivalent (AFAICT):
A lot of the discussion regarding this has been about "casts" but even if one avoids casts, one could still be affected by this.
danielhenrymantilla commentedon May 24, 2024
Yes, it applies to "ref-to-ptr coërcions" as well. I think the "cast" terminology is used here kind of indiscriminately to cover both explicit
as ...casts, and implicit casts, such as the ones stemming from "coërcions" like withlet ptr: *const u8 = &val[0];oridentity::<*const u8>(&val[0]). The terminology in the book about casts vs. coërcions is a bit muddy, to be honest.RalfJung commentedon May 24, 2024
Yes indeed, we often view coercions as just automatically inserted casts. Opsem discussions generally are happening on a level where all these implicit operations are made explicit. I can see how that can make the terminology confusing though.
A248 commentedon Jan 2, 2026
I'm developing a library and ran into this.
The rules were very confusing at first, and I kept running into SB violations, but I think I understand better now. Let me describe what I did for others who are interested -- and if I'm wrong, one of the seniors can correct me.
Originally I was taking a reference to the array element and coercing it to a pointer, but that violates SB. Fortunately, I was able to adapt @saethlin 's example code and look at the linked issues, and derive a solution that made MIRI happy. The trick is to take the pointer from the big allocation, then use pointer arithmetic to arrive at the field's pointer. This way, the computed field pointer inherits the access from the big struct's pointer (which grants access to the whole allocation).
Importantly, my initial attempts to make this work kept failing. I had an
Arcpointing to an allocated slice DST, and I wanted to take pointers to array elements and "rewind" them to the start of the allocation later. However, my code kept taking references that I wasn't aware of. The final solution (to get a correctly permissive pointer) looked like this:bitref: bit-level reference types
bitref: bit-level reference types
bitref: bit-level reference types
bitref: bit-level reference types
bitref: bit-level reference types
bitref: bit-level reference types
bitref: bit-level reference types