Provenance is a hard problem, so I'm probably missing something here
Provenance is a hard problem, so I'm probably missing something here
Posted Oct 8, 2025 18:19 UTC (Wed) by NYKevin (subscriber, #129325)In reply to: Provenance is a hard problem, so I'm probably missing something here by daroc
Parent article: Progress on defeating lifetime-end pointer zapping
I don't think that works.
&dyn Trait, and everything derived from it (such as &UnsafeCell<dyn Trait>) is a fat pointer. It has one pointer to the object, and a separate pointer to a statically allocated vtable. This vtable exists per type, not per instance, so you can't overwrite it, and swapping it for a different vtable is considered a mutation of the pointer, not a mutation of the pointee.
The other, more prosaic issue with this is that two different trait objects (with the same trait bound) are not necessarily the same size, so swapping them might not even be possible (the larger object will not fit into the smaller object's allocation).
Of course, if you have UnsafeCell<&dyn Trait>, then you can mutate the pointer rather than the pointee, but that's not specific to trait objects (and it seems rather obvious to me that you can't do this optimization if the pointer could be mutated out from under you).
Posted Oct 8, 2025 18:34 UTC (Wed)
by daroc (editor, #160859)
[Link] (1 responses)
You're right; sorry, I was unclear. I meant that if you had a type like this:
... then when you lock the Mutex and call .foo_method() on it, the Deref implementation is going to have to go get the vtable pointer from the &dyn Foo; if you then pass the mutex guard to another function and/or unlock and relock it, before calling .foo_method() again, the compiler will have to fetch the vtable pointer from inside the structure again, instead of re-using the cached value.
I think. I'm fairly certain that that's correct, and I think it's analogous to the mentioned C++ example, but I might be missing a nuance.
Posted Oct 8, 2025 21:33 UTC (Wed)
by NYKevin (subscriber, #129325)
[Link]
Another way of thinking about it is that, if we ignore the Mutex (and const correctness) for the sake of simplicity, Arc<&dyn Foo> is morally equivalent to std::shared_ptr<Foo*> (where Foo has a vtable), not to be confused with the more usual std::shared_ptr<Foo>. The mutation you are describing is equivalent to replacing the Foo* with an entirely different Foo* that happens to point at an entirely different Foo instance. The mutex is needed to do this properly, of course, but it just adds another layer of indirection without really changing anything.
You might wonder what happens if we instead have Arc<Mutex<dyn Foo>>, which is of course an entirely legitimate type to construct. That allows you to get as far as obtaining &mut dyn Foo, but you can't get any further from there. You would need to call something like std::mem::swap() to replace it with a different Foo. But you can't do that, because dyn Foo is unsized and std::mem::swap() has an implied T: Sized bound, so the compiler will reject the call. All other (safe) functions or methods for doing this will either have a similar bound (usually implied), or will take T by value (which can't accept an unsized type), because there is no reasonable way to guarantee that the new Foo is small enough to fit into the old Foo's allocation.
Provenance is a hard problem, so I'm probably missing something here
Arc<Mutex<&dyn Foo>>
Provenance is a hard problem, so I'm probably missing something here