|
|
Subscribe / Log in / New account

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 12:55 UTC (Wed) by daroc (editor, #160859)
In reply to: Provenance is a hard problem, so I'm probably missing something here by mb
Parent article: Progress on defeating lifetime-end pointer zapping

In Rust, if the function takes ownership of its parameter, the calling function can't use it afterward. Conversely, if the function takes its parameter by reference, it can't free the object. So yes, this problem is ruled out at compile time.

... unless your type has a mutable interior cell that stores a trait object. In that case the function could swap out the trait object for a different one with a different vtable, and the calling code would have to re-load the pointer to the vtable. But the compiler can tell whether that's a possibility by inspecting the types involved.


to post comments

Provenance is a hard problem, so I'm probably missing something here

Posted Oct 8, 2025 18:19 UTC (Wed) by NYKevin (subscriber, #129325) [Link] (2 responses)

> ... unless your type has a mutable interior cell that stores a trait object. In that case the function could swap out the trait object for a different one with a different vtable, and the calling code would have to re-load the pointer to the vtable. But the compiler can tell whether that's a possibility by inspecting the types involved.

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).

Provenance is a hard problem, so I'm probably missing something here

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:

    Arc<Mutex<&dyn Foo>>

... 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.

Provenance is a hard problem, so I'm probably missing something here

Posted Oct 8, 2025 21:33 UTC (Wed) by NYKevin (subscriber, #129325) [Link]

That is correct as far as I'm aware, but the critical point is that you are describing a mutation of the pointer to point at a different trait object altogether, whereas C++ placement new has the effect of destroying the pointee and constructing a new one in its place, without touching the pointer at all.

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.


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds