Skip to content
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

Tracking issue for {Rc, Arc}::get_mut_unchecked #63292

Open
1 task
Tracked by #7
SimonSapin opened this issue Aug 5, 2019 · 37 comments
Open
1 task
Tracked by #7

Tracking issue for {Rc, Arc}::get_mut_unchecked #63292

SimonSapin opened this issue Aug 5, 2019 · 37 comments
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. Libs-Tracked Libs issues that are tracked on the team's project board. requires-nightly This issue requires a nightly compiler in some way. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@SimonSapin
Copy link
Contributor

SimonSapin commented Aug 5, 2019

On Rc and Arc an new unsafe get_mut_unchecked method provides &mut T access without checking the reference count. Arc::get_mut involves multiple atomic operations whose cost can be non-trivial. Rc::get_mut is less costly, but we add Rc::get_mut_unchecked anyway for symmetry with Arc.

These can be useful independently, but they will presumably be typical when uninitialized constructors (tracked in #63291) are used.

An alternative with a safe API would be to introduce UniqueRc and UniqueArc types that have the same memory layout as Rc and Arc (and so zero-cost conversion to them) but are guaranteed to have only one reference. But introducing entire new types feels “heavier” than new constructors on existing types, and initialization of MaybeUninit<T> typically requires unsafe code anyway.

PR #62451 adds:

impl<T: ?Sized> Rc<T> { pub unsafe fn get_mut_unchecked(this: &mut Self) -> &mut T {} }
impl<T: ?Sized> Arc<T> { pub unsafe fn get_mut_unchecked(this: &mut Self) -> &mut T {} }

Open questions

@SimonSapin SimonSapin added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. labels Aug 5, 2019
@Centril Centril added the requires-nightly This issue requires a nightly compiler in some way. label Aug 5, 2019
@ghost
Copy link

ghost commented Sep 12, 2019

These functions should be renamed to get_unchecked_mut for consistency with other similar APIs in the standard library. See for example: https://doc.rust-lang.org/nightly/std/pin/struct.Pin.html#method.get_unchecked_mut

@CAD97
Copy link
Contributor

CAD97 commented Nov 22, 2019

Are the safety docs for this correct? They currently say

Any other Arc or Weak pointers to the same allocation must not be dereferenced for the duration of the returned borrow. This is trivially the case if no such pointers exist, for example immediately after Arc::new.

This means that this is sound:

let mut arc = Arc::new(0);
let clone = Arc::clone(&arc);
unsafe {
    *Arc::get_mut_unchecked(&mut arc) = 1;
}
if *clone == 1 {
    println!("1"); // this branch is always taken
} else {
    println!("0");
}

I don't think this is necessarily incorrect (and Miri passes), but it should be noted that ArcInner doesn't put the data payload in an UnsafeCell, and it is always behind a ptr::NonNull. In fact, I've re-convinced myself that this is correct while writing this comment, but I think it could be more directly called out in the docs that this is sound. (It's subtle why.) This is new surface area to (A)Rc that isn't exposed on stable, and isn't replicated by a Unique(A)Rc type.

@SimonSapin
Copy link
Contributor Author

As far as I understand, the absence of UnsafeCell is ok here because we’re not mutating through a &_ shared reference.

Can you say some more about what is subtle and how you conclude that this is sound?

@CAD97
Copy link
Contributor

CAD97 commented Nov 22, 2019

The main thing is that I previously considered an Arc to be "just" a shared box, where get_mut_unchecked exposes the falsehood of that understanding via allowing use of Arc for (completely unchecked) internal mutability.

I just found it nonobvious that Arc enables shared mutability, that's really it. Without this API it only exposes unshared mutability (of the payload).

@SimonSapin
Copy link
Contributor Author

We previously had get_mut and make_mut that allowed mutability through Arc, though yes that is only with (runtime checking that) no sharing (is) happening.

@SimonSapin
Copy link
Contributor Author

I initially documented this as safe when there is only one reference (no sharing), then Ralf pointed out this can be relaxed: #62451 (comment)

@CAD97
Copy link
Contributor

CAD97 commented Dec 10, 2019

I wrote a bit more about this over on irlo. TL;DR I really think making the RcBox/ArcInner types { strong: UnsafeCell<usize>, weak: UnsafeCell<usize>, value: UnsafeCell<T> } is a good change for clarity of implementation and avoiding problems like Rc::get_mut(Rc::from_raw(Rc::into_raw(_))) potentially being unsound.

@CAD97
Copy link
Contributor

CAD97 commented Dec 10, 2019

cc @RalfJung on this tracking issue since it plays into pointer provenance rules.

@RalfJung
Copy link
Member

RalfJung commented Dec 10, 2019

avoiding problems like Rc::get_mut(Rc::from_raw(Rc::into_raw(_))) potentially being unsound.

The fix for that, IMO, is to not go through deref in into_raw but use a raw ptr to the RcBox/ArcInner directly.

@CAD97
Copy link
Contributor

CAD97 commented Dec 15, 2019

https://internals.rust-lang.org/t/rc-and-internal-mutability/11463/13

get_mut_unchecked can cause a lifetime to be too short while still meeting the requirement of "no other rc types are 'used' over the mutation":

#![feature(get_mut_unchecked)]

use std::rc::Rc;

unsafe fn f<'a>(mut r: Rc<&'a str>, s: &'a str) {
    *Rc::get_mut_unchecked(&mut r) = &s;
}

fn main() {
    let x = Rc::new("Hello, world!");
    {
        let s = String::from("Replaced");
        unsafe { f(x.clone(), &s) };
    }
    println!("{}", x);
}

Found by @xfix

So either a "this doesn't do bad things with lifetimes" rule needs to be added, or it should go back to only allowing when get_mut would succeed.


Later edit: for a clearer picture of what is happening, see a version with the reference-of operator lifted. Changing it from &mut x to &mut x.clone() allows it to compile, as the cloned version can reduce its lifetime's length.

@SimonSapin
Copy link
Contributor Author

Good find! As far as I can tell this happens because Rc is covariant. x and x.clone() are Rc<&'static str>, but covariance says it’s fine to pass the latter as Rc<&'a str> with a smaller lifetime. But because there’s sharing between the two strong references, the program can then incorrectly re-interpret s as &'static str.

This wouldn’t happen with Rc::get_mut because:

  • If there is any sharing, it will return None and not allow mutation.
  • If there is not, then there is nothing left with Rc<&'static str>. We’ve permanently reduced the lifetime of that particular RcBox. If for example f returns Rc<&'a str>, the borrow checker won’t let main extend that return value back to Rc<&'static str>.

This wouldn’t happen with Rc<RefCell<&'static str>> because it can only be passed as Rc<RefCell<&'a str>> if 'a == 'static, because RefCell is invariant.

Making Rc invariant would fix this soundness issue but break some existing stable programs.

So either a "this doesn't do bad things with lifetimes" rule needs to be added,

Unfortunately "bad things" is hard to define precisely because it involves talking about variance, and NonNull seems to be the the only standard library item so far to do that in its docs. Checking whether a program does "bad things" is also hard because it involves non-local reasoning.

or it should go back to only allowing when get_mut would succeed.

I think this sounds more reasonable.

@RalfJung, what do you think?

@Kixunil
Copy link
Contributor

Kixunil commented Dec 16, 2019

But introducing entire new types feels “heavier” than new constructors on existing types, and initialization of MaybeUninit<T> typically requires unsafe code anyway.

Consider this example:

/// Safe abstraction for initializing array
struct Array<T, const N: usize> {
arr: [MaybeUninit<T>; N],
position: usize
}

impl<const N: usize> Array<u8, N> {
    /// Helper for safely, efficiently reading from reader
    read<R: Read>(&mut self, reader: R) -> Result<usize, Error> {
        // ...
    }
}

This abstraction is completely safe, but can't be used behind Arc or Rc without going through unsafe. And sure, other crates can implement a safe abstraction, but it causes these issues:

  • Inexperienced people might get it wrong and cause UB
  • It becomes less discoverable, leading to higher probability of people reimplementing it, leading to higher probability of the above
  • If multiple crates from above happen to make same kind of mistake (as happened recently), it has to be reported several times and the reporter must know of all the cases.

I believe Rust standard libraries should provide important safe abstractions such as this by default.

@SimonSapin
Copy link
Contributor Author

I believe Rust standard libraries should provide important safe abstractions such as this by default.

I agree with that principle, only I don’t always know which abstractions are important enough. If you have a particular one in mind feel free to also propose it in a new PR or RFC.

@CAD97
Copy link
Contributor

CAD97 commented Dec 16, 2019

FWIW, I have mostly-complete WIP single-file library implementations of (A)RcBox (Unique(A)Rc)) and (A)RcBorrow. The first step to an RFC for either of these types would be to polish these library implementations and publish them. (The second would probably be to show their utility as vocabulary types.) (The advantage of these impls over triomphe is compatibility with std (A)Rc.)

@SimonSapin
Copy link
Contributor Author

There’s also https://crates.io/crates/triomphe (which was initially started to remove weak references)

@Kixunil
Copy link
Contributor

Kixunil commented Dec 16, 2019

Fair enough. I don't happen to need it right now (I experienced a similar situation as I was describing with some C code before), but I will keep in mind contributing if I happen to need it.

@RalfJung
Copy link
Member

@xfix @CAD97 good catch!

@RalfJung, what do you think?

I'd say the "bad thing" that happens here is that a "bad value" is written into the Rc -- one that doesn't match the type signature expected by all people with references to this Rc instance.

For the docs, I think we could explain this. But if you think that is too complicated, I'm also fine saying something like "to be safe, make sure this is the only Rc" or so. But it should be clear (at least when looking at the source and not just the docs) that this is sufficient for safety but not necessary -- that there are ways to safely use this that involve other Rc existing, but that doing so is full of traps and it is hard to even say exactly what needs to be taken care of.

@KodrAus KodrAus added the Libs-Tracked Libs issues that are tracked on the team's project board. label Jul 30, 2020
@KodrAus
Copy link
Contributor

KodrAus commented Jul 31, 2020

Since this is an unsafe, unchecked, optimization API, updating the docs to mention being careful around variance with some examples of what to watch out for seems like enough to me.

@SoniEx2
Copy link
Contributor

SoniEx2 commented Aug 17, 2021

re: #63292 (comment)

so this is why it's unsound to have Arc::borrow_mut with Weak references around... unless we had an Invariant type and Arc::borrow_mut was an impl on Arc<Invariant<T: ?Sized>>? (Arc<Cell<T>> would also kinda work but Cell isn't Send+Sync sadly.)

@JakobDegen
Copy link
Contributor

JakobDegen commented Jan 20, 2022

The docs as written do not sufficiently describe the safety concerns. For example, the following code is definitely UB:

let mut a = Rc::new(5_i32);
let b = Rc::clone(&a);

let violated_ref: &i32 = b.deref();
unsafe {
     *Rc::get_mut_unchecked(&mut a) = 10;
};

println!("{}", *violate_ref);

The Rc was never dereferenced during the get_mut_unchecked, but still this is an aliasing violation, as the value behind a &i32 changes within its lifetime.

Essentially, this change would turn every Arc<T> into an Arc<UnsafeCell<T>>. We could never allow LLVM to insert spurious reads to an Arc. On top of that, I have no idea how any code expects to prove that the thing in the example above doesn't happen without holding the only Arc referencing that data. If a user really wants to mutate stuff within a type that is usually immutable, the potential footguns should be made clear to them by forcing them to use an appropriate Arc<CellVariant<T>>.

The correct approach here is imo to require the same safety contract as get_mut, ie that this be the only Arc referencing the data. If not, at the very least this function should be renamed because it is definitely not just an unchecked version of that function - get_mut on its own never allows these kinds of patterns.

@JakobDegen
Copy link
Contributor

JakobDegen commented Jan 20, 2022

Also relevant to note is that existing APIs may have already been doing things semi-equivalent to this:

fn use_rc(&mut self, b: Rc<i32>) {
     self.p = b.deref() as *const _;
     self.rc = b;
}

Writing code like this:

let a = Rc::new(5);
something.use_rc(Rc::clone(&a);
Rc::get_mut_unchecked(&mut a);

is now UB. This is not technically a breaking change, but it still feels weird to add a method that is UB in conjunction with many existing APIs for that type in a way that is sort of unclear. There's an example of this in a recent crate I wrote (that example also shows that the .deref() call is not always avoidable in this kind of situation.)

@CAD97
Copy link
Contributor

CAD97 commented Jan 20, 2022

@JakobDegen

In this comment I use Rc to refer to both Rc and Arc, and RcInner to refer to the heap part of Rc (which are currently RcBox and ArcInner in the stdlib code).

the following code is definitely UB:

Just to be clear up front: yes. This example is clearly UB, and we agree here.

The Rc was never dereferenced during the get_mut_unchecked

It depends on how exactly you define dereferenced. If you define it as the instantaneous act of calling Deref::deref(&it), ending when the method returns, then yes, it wasn't dereferenced while the get_mut_unchecked reference was alive. However, if you define dereferenced as the borrow lifetime generated by the Deref::deref call still being live, then it was dereferenced for the entire duration of the get_mut_unchecked reference.

It's this latter definition, where the condition to be avoided is "a reference to the interior is alive," that needs to be upheld. I agree that the documentation could make this clearer.

Essentially, this change would turn every Arc<T> into an Arc<UnsafeCell<T>>. LLVM would no longer be allowed to insert spurious reads to an Arc.

I've actually previously argued that Rc should store T inside RcInner as UnsafeCell<T>. It's the case that get_mut makes Rc a shared mutability type already! However, because Rc itself is ptr::NonNull<RcInner<T>>, no UnsafeCell is required; the pointer indirection is already enough to enable the shared mutability.1

Rc<T> itself doesn't expose shared mutability—get_mut correctly requires &mut Rc—but as far as the abstract machine and LLVM are concerned, Rc<T> enables shared mutability the same way *mut T raw pointers do. Now, if Rc used &'static RcInner<T>2, things would be different, and it definitely would need to store T as UnsafeCell<T>, even just for get_mut.

Rc::get_mut_unchecked does expose Rc as a shared mutability type, but this is accurate to what's actually the case. This operation is already possible and language-sound for an owned Rc via into_raw, even if not necessarily documented as okay, thus being library-UB.

Perhaps we should even loosen the function signature further to Rc::get_mut_unchecked(&Rc<T>) -> &mut T to drive home the point further. (You can already Rc::get_mut_unchecked(&mut it.clone()), modulo temporary lifetimes.)

On top of that, I have no idea how any code expects to prove that this doesn't happen without holding the only Arc referencing that data.

I agree that using this correctly outside of when get_mut is valid is difficult, but it's clearly possible. A simple example where it's clearly okay is directly after initialization with new_cyclic; you have Weaks that exist, making get_mut fail, but know that no borrows are active yet.

The correct approach here is imo to require the same safety contract as get_mut, ie that this be the only Arc referencing the data.

This function was actually originally documented as only being valid when get_mut succeeds. The documented invariant was relaxed specifically because that constraint is more restrictive than it needs to be; the constraint of "no other active borrows" is the correct constraint.

If not, at the very least this function should be renamed because it is definitely not just an unchecked version of that function - get_mut on its own never allows these kinds of patterns.

The fact of the matter is that get_mut checks for an easily checkable state that guarantees mutable access to be sound. While this is sufficient for the access to be sound, there are other, harder to check for conditions that make the operation sound. It's this class of conditions that are allowed in get_mut_unchecked, even though the checked version is conservative.

Footnotes

  1. I actually still hold that an UnsafeCell would be a good clarification here, even if it's not strictly necessary. My (very undeveloped, prototype) experimental implementation of Rc and Arc as two generic strategy instantiations on top of a shared base (goal: reducing the copy/paste feeling between Rc and Arc) does so.

  2. Potentially a good application of the theoretical &unsafe? (&'unsafe TL;DR: was a valid reference, may have been invalidated, another source dereference (is unsafe and) proves that it hasn't been invalidated yet. Thus spurious loads are allowed (if justified by a later actual source load) because it otherwise behaves exactly like a normal reference, just programmer rather than machine checked lifetime.) &'static is unsound here because you have to dealloc in drop... though perhaps the #[may_dangle] eyepatch can make &'static not unsound here? (I'm still very unclear on how exactly #[may_dangle] works. I don't think I'll ever fully understand it, tbf.)

@CAD97
Copy link
Contributor

CAD97 commented Jan 20, 2022

Also relevant to note is that existing APIs may have already been doing things semi-equivalent to this:

fn use_rc(&mut self, b: Rc<i32>) {
     self.p = b.deref() as *const _;
     self.rc = b;
}

This really looks like it's wanting for Rc::as_ptr, which doesn't clash/invalidate with get_mut_unchecked.

@JakobDegen
Copy link
Contributor

Also relevant to note is that existing APIs may have already been doing things semi-equivalent to this:

fn use_rc(&mut self, b: Rc<i32>) {
     self.p = b.deref() as *const _;
     self.rc = b;
}

This really looks like it's wanting for Rc::as_ptr, which doesn't clash/invalidate with get_mut_unchecked.

See the example code I linked for why that wouldn't actually help in slightly more complicated (but still very practical and very realistic) code.

It depends on how exactly you define dereferenced.

I mean, I suppose, but I don't buy at all that "dereferenced" is a state instead of an operation.

It's the case that get_mut makes Rc a shared mutability type already!

It is definitely not. As a matter of fact, the pre-condition on that method makes it very clear that it the mutability is specifically not shared. The fact that the pointer returned by into_raw() has write permissions is an unfortunate side-effect of the existence of get_mut(), and definitely an implementation detail.

With regards to the rest of your response: For those cases, it really sounds like stuff should just be inside a MaybeUninit<T> and/or UnsafeCell<T>. I see no good reason to encourage people to hide the sharing/partial intialization from the type system.

@Kixunil
Copy link
Contributor

Kixunil commented Jan 20, 2022

How about changing the documentation to say this:

No active reference to the same allocation may exist for the duration of returned borrow. Since this may be hard to get right it's recommended to only use this method when the strong reference count is guaranteed to be 1 and weak reference count is guaranteed to be 0 (when get_mut() would have returned Some(_)).

Anyway, even better to have the safe API and just point people to it.

@KamilaBorowska
Copy link
Contributor

Anyway, even better to have the safe API and just point people to it.

That safe API is get_mut and it already exists.

@Kixunil
Copy link
Contributor

Kixunil commented Jan 20, 2022

@xfix not really, we were talking about RcMut and ArcMut above which don't have performance overhead and statically don't panic.

@CAD97
Copy link
Contributor

CAD97 commented Jan 20, 2022

It's the case that get_mut makes Rc a shared mutability type already!

It is definitely not.

(This was worded a bit more strongly than intended due to a prior draft before I fixed up some accidental issues due to fallible memory.) The intent here was to note that Rc allows for internal mutability as part of its current implementation. I agree that it doesn't expose any sort of shared mutability in the currently stable API, though, and this API adds shared mutability to the API where it didn't exist previously.

I think the two reasonable approaches here are

  • Embrace that Rc, as a shared ownership type, allows for (completely unchecked) shared mutability, clarify the wording, document the variance gotchas, and change the API to fn(&Rc<T>) -> &mut T; or
  • Maintain that Rc only supports unique mutability, and have the safety requirement that get_mut would succeed (thus making it possible to potentially mark Rc's pointer as @dereferencable through some future language feature).

Personally, I think it's fine if a projected rc makes get_mut_unchecked unavailable (in fact, it makes get_mut unsound, so it's not surprising that get_mut_unchecked is) even though an "inactive" regular rc doesn't. The question, though, is how to communicate that properly, since the projected rc (Mrc) is doing weird things with StableDeref-interior reference lifetimes.

RcMut and ArcMut

(Btw, my rc-box crate is publicly available now and provides a safe API for known-unique Rc handles, and implements DerefMut.)

@Kixunil
Copy link
Contributor

Kixunil commented Jan 20, 2022

I think that making shared mutability public is just breaking separation of concerns which is generally followed in Rust. People who want it should just use UnsafeCell<T>. Keeping the possibility of improving optimizations is also good. "Don't pay for what you don't use."

my rc-box crate is publicly available now and provides a safe API for known-unique Rc handles

It's really great that you made it and I think it should be included in the standard library because there were already multiple cases of people getting unsafe subtly wrong when adding the required APIs to std/alloc/core would have prevented it. Discoverability is important here.

@zachs18
Copy link
Contributor

zachs18 commented Sep 2, 2022

In addition to variance, this also has problems with the existing (stabilized in 1.62.0) impl From<Rc<str>> for Rc<[u8]> (likewise for Arc), since an Rc<str> can be cloned, converted into an Rc<[u8]>, get_mut_uncheckeded, and filled with invalid UTF-8, which can later be observed in the original Rc<str>. This should probably be mentioned in the docs as well as the variance issues (if the docs aren't changed to disallow other Rc/Weaks to the same allocation).

playground link

#![feature(get_mut_unchecked)]
use std::rc::Rc;

fn main() {
    let a: Rc<str> = "Hello, world!".into();
    let mut b: Rc<[u8]> = a.clone().into();
    unsafe {
        Rc::get_mut_unchecked(&mut b).fill(0xc0); // any non-ascii byte
    }
    dbg!(&a);
}

(Explanation of linked bytemuck PR)

(skipping over size and alignment for simplicity)

Bytemuck allows converting a &T to a &U if T has no uninit bytes, and U allows any bit pattern. This is sound because there is no way to change the U such that it would become an invalid T (bytemuck categorically disallows interior mutability).
Conceptually, Rc<T> -> Rc<U> should work similarly (either the U cannot be changed, or the only Rc is the U, so either way no T can observe invalid values), but Rc::get_mut_unchecked breaks that (if its safety contract allows other Rc/Weaks to exist), because the U can be changed while an Rc<T> still exists. So to be able to convert Rc<T> -> Rc<U> requires both T and U have no uninit bytes and allow any bit pattern. (currently).

@Kixunil
Copy link
Contributor

Kixunil commented Sep 2, 2022

I think regarding these safety issues it'd be better if people explicitly used UnsafeCell to signal the intent. Not allowing shared mutability may allow some future optimizations and we already have a construct for shared mutability.

@ctemple
Copy link

ctemple commented Oct 28, 2022

I think it's necessary because &self: Rc

@SimonSapin
Copy link
Contributor Author

In #101310 (comment) I commented:

Given how finicky they are to use correctly, at this point I’d be in favor of removing these functions entirely and perhaps replace them with UniqueArc / UniqueRc types that would represent "Arc/Rc with strong_count == 1 && weak_count == 0", implement DerefMut, and have zero-cost conversion to Arc/Rc

@Kixunil
Copy link
Contributor

Kixunil commented Nov 20, 2022

@SimonSapin so can I make a PR? :)

@mejrs
Copy link
Contributor

mejrs commented Nov 20, 2022

I agree with that. I've seen several people use it, and none used it correctly. They look too much like "this is what I need" and they are too hard/tricky to use correctly.

Overall I think it would be better if these methods did not exist, and that if stabilized I feel like we'd regret it.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Nov 20, 2022
…soundness, r=Mark-Simulacrum

Clarify and restrict when `{Arc,Rc}::get_unchecked_mut` is allowed.

(Tracking issue for `{Arc,Rc}::get_unchecked_mut`: rust-lang#63292)

(I'm using `Rc` in this comment, but it applies for `Arc` all the same).

As currently documented, `Rc::get_unchecked_mut` can lead to unsoundness when multiple `Rc`/`Weak` pointers to the same allocation exist. The current documentation only requires that other `Rc`/`Weak` pointers to the same allocation "must not be dereferenced for the duration of the returned borrow". This can lead to unsoundness in (at least) two ways: variance, and `Rc<str>`/`Rc<[u8]>` aliasing. ([playground link](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=d7e2d091c389f463d121630ab0a37320)).

This PR changes the documentation of `Rc::get_unchecked_mut` to restrict usage to when all `Rc<T>`/`Weak<T>` have the exact same `T` (including lifetimes). I believe this is sufficient to prevent unsoundness, while still allowing `get_unchecked_mut` to be called on an aliased `Rc` as long as the safety contract is upheld by the caller.

## Alternatives

* A less strict, but still sound alternative would be to say that the caller must only write values which are valid for all aliased `Rc`/`Weak` inner types. (This was [mentioned](rust-lang#63292 (comment)) in the tracking issue). This may be too complicated to clearly express in the documentation.
* A more strict alternative would be to say that there must not be any aliased `Rc`/`Weak` pointers, i.e. it is required that get_mut would return `Some(_)`. (This was also mentioned in the tracking issue). There is at least one codebase that this would cause to become unsound ([here](https://github.com/kaimast/lsm-rs/blob/be5a164d770d850d905e510e2966ad4b1cc9aa5e/src/memtable.rs#L166), where additional locking is used to ensure unique access to an aliased `Rc<T>`;  I saw this because it was linked on the tracking issue).
@CAD97
Copy link
Contributor

CAD97 commented Nov 21, 2022

UniqueArc / UniqueRc types

The way to do so would probably be

but that was previously abandoned due to compile time regressions, which seem to be due just to LLVM having more code to chew through in the widely monomorphized allocation path.

It's possible that the intervening LLVM upgrades and MIR inlining improve the situation there, if someone wants to try reviving that PR. (Unfortunately I'm too busy and have no idea how to go about diagnosing and addressing the regression if it's not just magically gone.)

(For wanderers, I also have a crate providing known-unique versions of (A)Rc.)

@SimonSapin
Copy link
Contributor Author

It’s one way but I think it doesn’t have to? UniqueRc could be a wrapper type for Rc without changing its internals, only adding safety invariants

thomcc pushed a commit to tcdi/postgrestd that referenced this issue Feb 10, 2023
…, r=Mark-Simulacrum

Clarify and restrict when `{Arc,Rc}::get_unchecked_mut` is allowed.

(Tracking issue for `{Arc,Rc}::get_unchecked_mut`: #63292)

(I'm using `Rc` in this comment, but it applies for `Arc` all the same).

As currently documented, `Rc::get_unchecked_mut` can lead to unsoundness when multiple `Rc`/`Weak` pointers to the same allocation exist. The current documentation only requires that other `Rc`/`Weak` pointers to the same allocation "must not be dereferenced for the duration of the returned borrow". This can lead to unsoundness in (at least) two ways: variance, and `Rc<str>`/`Rc<[u8]>` aliasing. ([playground link](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=d7e2d091c389f463d121630ab0a37320)).

This PR changes the documentation of `Rc::get_unchecked_mut` to restrict usage to when all `Rc<T>`/`Weak<T>` have the exact same `T` (including lifetimes). I believe this is sufficient to prevent unsoundness, while still allowing `get_unchecked_mut` to be called on an aliased `Rc` as long as the safety contract is upheld by the caller.

## Alternatives

* A less strict, but still sound alternative would be to say that the caller must only write values which are valid for all aliased `Rc`/`Weak` inner types. (This was [mentioned](rust-lang/rust#63292 (comment)) in the tracking issue). This may be too complicated to clearly express in the documentation.
* A more strict alternative would be to say that there must not be any aliased `Rc`/`Weak` pointers, i.e. it is required that get_mut would return `Some(_)`. (This was also mentioned in the tracking issue). There is at least one codebase that this would cause to become unsound ([here](https://github.com/kaimast/lsm-rs/blob/be5a164d770d850d905e510e2966ad4b1cc9aa5e/src/memtable.rs#L166), where additional locking is used to ensure unique access to an aliased `Rc<T>`;  I saw this because it was linked on the tracking issue).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. Libs-Tracked Libs issues that are tracked on the team's project board. requires-nightly This issue requires a nightly compiler in some way. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests