-
Notifications
You must be signed in to change notification settings - Fork 121
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
MutableHandle are clonable breaking safety provided by &mut #520
Comments
I think the proper solution would be to not impl Clone/Copy for *Handle or at least mark it as unsafe. |
I think the first action point would be to remove all |
I'm going to take a go at this. |
Ok. I assigned you. Do note that big changes such as this require companion PRs in servo. |
There is a larger problem here, Specifically pub struct RootedGuard<'a, T: 'a + RootKind> {
root: &'a mut Rooted<T>,
} Which requires that it have unique access to root. Meanwhile the entire purpose of this type is that it stores a root that spidermonkey is aware of, and can read and update, as part of its gc (which is enabled by adding it to a root stack during The same problem exists with anything that creates a How can we fix this? There are a few paths. In all cases we can't have an 1. Lean into interior mutability
2. Lean into raw pointersEverything can be transformed to look like the existing pub struct MutableHandle<'a, T: 'a> {
pub(crate) ptr: *mut T,
anchor: PhantomData<&'a mut T>,
} Instead of storing references, store raw pointers that don't have aliasing guarantees. There are suboptions here, as to what the API looks like:
3. Bypass rust's aliasing and provenance assumptionsReplace pointers to Because By the same token we can't access the impl<T> Tracked<T> {
// Note that it isn't possible to create a `Tracked<T>` directly. That has
// no meaning. We will only ever allow the creation of references, and
// pointers, to a `Tracked<T>`.
fn new(val: &mut T) -> &mut Self {
let ptr = val as *mut T;
ptr.expose_provenance();
&mut *(ptr as *mut Self),
}
pub fn as_ptr(&self) -> *const T {
ptr::with_exposed_provenance((self as *const Self).addr())
}
pub fn as_mut_ptr(&mut self) -> *mut T {
ptr::with_exposed_provenance_mut((self as *mut Self).addr())
}
pub fn get(&self) -> T
where
T: Copy
{
// SAFETY: Any accesses come from the same thread as the &mut self lives on,
// so there can be no sychronization issues.
unsafe{ *self.as_ptr() }
}
pub fn set(&mut self, v: T)
where
T: Copy
{
// SAFETY: Any reads come from the same thread as the &mut self lives on,
// so there can be no sychronization issues.
unsafe{ *self.as_mut_ptr() = v }
}
} Note Maybe some day rust will allow us to store provenance in a 0 sized struct and we could pass the correct provenance along. I don't believe that day is today. By implementing it with This option is similar to 2. and 3. in the "lean into raw pointers" proposals, except it avoids the syntactic noise, because we don't have an extra layer of indirection, and we get to take advantage of the compilers implicit reborrowing. Instead it comes with the drawback that we are relying on exposed provenance, which is not fully defined. It is also as far as I know novel, which is not really a good thing with unsafe code.
I came up with the As it stands though, the GC we have mutates values. Which means that while on the rust side we can mimic the "must have a unique pointer to write" semantics, we can't take advantage of any of the properties that provides us (like values not changing if the borrow checker says they don't). Considering that, it doesn't seem worthwhile to track whether or not a pointer is unique and only allow writing through unique pointers. Both because satisfying the borrow checker is actually a slight amount of extra work, and because all the ways to do that have drawbacks - either in terms of friction for users of the API or by using exposed provenance. I'd appreciate some feedback on this before going ahead and implementing that, both because I'm a newcomer to the project and it's a non-trivial change, and because this is a decision that feels like it may have subtle implications. Edit: I've re-evaluated as I've experimented over the last couple of days. I think I now favor the |
Two further thoughts that I didn't end up capturing in the above:
|
cc @jdm |
I've suspected we were more unsound issues due to aliasing rules, but I never really dive into it. Thanks for all the investigation work. I think we would still need to access handle.ref(|r: &T| {
// use static analysis lint to ensure no GC happens here
}); or some unsafe function where users would need too ensure that themself. It all boils down to notion of no_gc/can_gc that can only be provided via static analysis in the current model. I am questioning if it's really worth fixing current model (there are also other problems and ergonomics) or should we throw our resources at different model: https://github.com/asajeffrey/josephine where all &T have lifetime bounded to ctx, so when you try to do GC (or call any function that can call GC) you need &mut ctx, so you get no_gc via lifetimes (rough idea). This is still under construction and not battle tested, so we might not be able to afford experimenting with it due to limited resources. |
related: servo/rust-mozjs#273 |
Thank you for all this analysis! I would like to take the time to read it closely and think about it, but I'm currently sick and also solo parenting a toddler, so I can't promise if I will have time before the weekend (I really hope I'm less sick by this weekend). |
Take your time and get better! These problems have waited a decade, they can definitely wait a bit longer (not that you need permission from a stranger on the internet).
It occurs to me that the slash makes a big difference here. Both of these require preventing the GC from running (e.g. by borrowing context), but the Potentially I should try and implement this enough to hunt down the cases where this is necessary in servo and see what they look like. I'm sort of surprised I didn't see them when porting everything to use
Josephine looks interesting, I'll find some time to look at it more closely for inspiration if nothing else. My initial gut feeling is that we're more likely to settle on a good API by iterating on the thing with real use (this), than starting from scratch. ... I'll have more thoughts on this |
So I don't think this quite works. Spidermonkeys docs say
Which I think means this proposal would prevent you from ever having 2 values you allocated at the same time. Which seems devastating in terms of usability. However if instead of mutably borrowing the context GC triggering ops immutably borrow it, and handles borrow it with the same mutability as the handle, then we get that you never have a That's such a restrictive property on |
I think my explanation wasn't good enough. Here is how I imagined it:
impl ___<T> {
fn deref<'root: 'r, 'cx: 'r, 'r>(&'root self, &'cx cx) -> &'r T {
// ...
}
fn deref_mut<'root: 'r, 'cx: 'r, 'r>(&'root mut self, &'cx cx) -> &'r mut T {
// ...
}
} Now each SM function that can trigger GC should accept EDIT: This model assumes good SafeJSContext that is not clonable. |
Actually this is not a problem. While we can have two |
This code still compiles, wouldn't that be a problem? fn main() {
let mut root = Root(5);
let h = root.handle_mut();
let h2 = h.clone();
use_handle(&h);
use_handle(&h2);
} |
Without assigning blame I definitely misunderstood. I agree, this looks like a reasonable and useful API.
Yes, sort of. Not when use_handle only takes an Here's my toy example with copy and pasted definitions that miri will complain about https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=632c9a86d7c1a90b48f9577848e5a450 |
Ignore that. |
I've updated #559 to remove the This is the accompanying servo commit, turns out that we aren't meaningfully using the impls almost anywhere. The one place where we actually held a reference, we did so across a GC, so that had to go... but it only required duplicating a few lines of code to avoid it. The PR currently only addresses |
mut is required for https://doc.servo.org/mozjs/gc/root/struct.MutableHandle.html#tymethod.set, but this is still weird as MutableHandle is clonable so
&mut
does not help a lot with safety.Originally posted by @sagudev in servo/servo#34087 (comment)
When cloning MutableHandle we still have underlying pointer point to same location, so we can have two &mut that points to same location.
The text was updated successfully, but these errors were encountered: