Repository navigation
Description
This issue proposes a resolution to the last outstanding question blocking dyn upcast. It is also available on the initiative repository.
Background
We are trying to enable "upcasts" from a dyn Trait to its supertraits:
trait Foo {
fn foo_method(&self);
}
trait Bar: Foo {
fn bar_method(&self);
}
let x: &dyn Bar = /* ... */;
let y: &dyn Foo = x; // compilesThe key factor for these upcasts is that they require adjusting the vtable. The current implementation strategy is that the vtables for the Bar trait embed pointers to a Foo vtable within them:
+----------------------------+
| Bar vtable for some type T |
|----------------------------|
| Foo vtable | ----------> +------------------+
| foo_method | -----+ | Foo vtable for T |
| bar_method | --+ | |------------------|
+----------------------------+ | | | foo_method | ---+
| | +------------------+ |
| | |
| +---> <T as Foo>::foo_method <-+
v
<T as Bar>::bar_method
(this diagram is only meant to convey the general idea of the vtable
layout, and doesn't represent the exact offsets we would use etc;
in fact, with the current implementation,
the first supertrait is stored 'inline' and hence no load is required)
This way, given a &dyn Bar object, we convert its Bar vtable to the appropriate Foo vtable by loading the appropriate field.
Although we don't want to commit to a particular implementation strategy, we do want to leave room for this strategy. One implication is that performing an upcast may require loading from the vtable, which implies that the vtable must be a valid pointer to an actual Rust vtable. Although &dyn Bar references would always contain a valid vtable, the same is not necessarily true for a raw pointer like *const dyn Bar or *mut dyn Bar.
In the language today, we only support "noop upcasts" that don't affect the vtable, and these are safe (e.g., converting from *const dyn Foo + Send to *const dyn Foo). If we extend the set of upcasts to permit vtable-adjusting upcasts, like *const dyn Bar to *const dyn Foo, this implies that, for safe code at least, all *const dyn Trait values must have a valid vtable, so that we know we can safely load the required field and perform the upcast.
On the other hand, we do not generally require raw *mut T pointers to point to valid data. In fact, we explicitly permit them to have any value, including null, and only require that they point to valid data when they are dereferenced. Because dereferencing a raw pointer is an unsafe operation, it has always been considered safe to expose an arbitrary raw pointer to unsafe code -- the unsafety arises when you take a raw pointer from an unknown source and dereference it, since unless you can trace the origin of that pointer you can't possible guarantee that it is valid to dereference.
This brings us to the conflict:
- It has historically been safe to "release" a raw pointer to safe code, but not safe to receive one (since you cannot know if it is valid).
- It has historically been safe to upcast
*const dynvalues (e.g.,*const dyn Foo + Sendto*const dyn Foo).- Unlike the upcasts we are considering now, this upcast does not require changing the vtable at runtime, but the distinction is subtle for end-users.
- Moreover, there are future extensions (e.g., upcasting
*const dyn Foo + Barto*const dyn Foo) that would require adjusting the vtable much like the upcasts currently being stabilized.
Related future consideration: virtual method calls on raw pointers
There have been requests to extend traits with the option to include raw pointer methods:
trait PtrLike {
fn is_null(v: *const Self) -> bool;
}These methods would be useful when writing unsafe code because having an &self method requires satisfying the validity conditions of an &-reference, which may not be possible. If we did have such methods, however, it raises the question of whether it would be safe to invoke is_null on a *const dyn PtrLike reference. Just as with upcasting, invoking a method from the vtable requires loading from the vtable, and hence requires a valid vtable generated by the compiler.
The solution we propose in this document also resolves this future dilemma.
Definitions: validity vs safety invariant
We adopt the terms validity and safety invariant from the unsafe code guidelines:
The validity invariant is an invariant that all data must uphold any time it is accessed or copied in a typed manner. This invariant is known to the compiler and exploited by optimizations such as improved enum layout or eliding in-bounds checks.
The safety invariant is an invariant that safe code may assume all data to uphold. This invariant is used to justify which operations safe code can perform. The safety invariant can be temporarily violated by unsafe code, but must always be upheld when interfacing with unknown safe code. It is not relevant when arguing whether some program has UB, but it is relevant when arguing whether some code safely encapsulates its unsafety -- in other words, it is relevant when arguing whether some library is sound.
In short, the validity invariant defines a condition that must always be true, even in unsafe code, and the safety invariant defines an invariant that unsafe code must guarantee before a value can be released to be used by arbitrary code.
Contours of the solution space
We can fix this conflict in one of two basic ways:
First, we could make vtable-adjusting upcasts casts (and *Self method calls) unsafe. This is difficult to implement and would require changes to the Coerce trait, which is already excessively complicated. In exchange, it offers at best marginal benefit: raw *dyn pointers can be released to safe code, but safe code can't do anything interesting with them. For this reason, we do not recommend this option.
If vtable-adjusting casts (and *Self method calls) are safe, then the safety invariant for *dyn types must be that their metadata points to a fully valid vtable (i.e., a vtable created by the compiler). This ensures safe code can perform upcasts or dynamic dispatch. This also implies that std::ptr::null (which is safe) cannot be extended to T where T: ?Sized unless further changes are made, since we would need to provide a valid vtable (it would be possible to permit a sentinel value, like null, to be used, but that would imply that upcasting must have a branch, making it less efficient).
There are, however, various options for the validity invariant, ranging from no invariant to requiring a fully valid vtable at all times. The strict invariants offer some benefits, such as the ability to have a niche for *dyn pointers. We survey the options here:
Validity invariant for *dyn metadata |
Supports niche | Can be initialized with std::mem::zeroed |
Constant initializer |
|---|---|---|---|
| None | ✅ | ✅ | |
| Word-aligned | ✅ | ✅ | ✅ |
| Word-aligned, non-null | ✅ | ✅ | |
| Valid vtable | ✅ |
Explanations for the column titles:
- Validity invariant for
*dynmetadata -- describes the invariant that applies to the metadata for a*dynvalue. - Supports niche -- true if there is a niche value so that
sizeof(Option<*const dyn Foo>) == sizeof(*const dyn Foo). - Can be initialized with
std::mem::zeroed-- true ifstd::mem::zeroedcan be used to create a valid value (very convenient). This makes it trivial to innitialize a*const dynwith a dummy value, though the value cannot be released to safe code. - Constant initializer -- true if there is some constant value for
*const dyn Foothat satisfies the validity invariant, no matter the traitFoo. This makes it easy to initialize a*const dynwith a dummy value, though the value cannot be released to safe code.
Other points:
*dynvalues currently have a niche.- Other pointer-like values (such as
fnand&-references) are expected to have a validity invariant of word-aligned, non-null. - Whichever option we use, we can backwards-compatibly move "up the table" and adopt a less-strict validity invariant without introducing UB into any extant programs.
Proposal
The proposal is as follows:
- Vtable-adjusting upcasts are safe operations. The upcast is UB if performed on a value without a valid vtable
- As such, the "safety invariant" requires a fully valid vtable.
- The "validity invariant" for
*dynmetadata is not yet defined; the recommendation to the opsem team is that we retain the ability for*const dyn Footo have a niche by requiring the vtable be word-aligned and non-null but nothing else (this clause changed over the course of discussion). Discussion in the opsem team is happening in here.
Vtable-adjusting upcasts are defined as:
- Trait upcasts that alter the set of methods that can be invoked on the resulting value at runtime (e.g.,
dyn Bartodyn Foofrom the introduction). In particular, upcasts that simply add or remove auto-traits are not vtable-adjusting (e.g.,dyn Debug + Sendtodyn Debug).
This approach...
- permits safe upcasting (and method calls, in the future);
- preserves the existing niche for
*const dyn; - is consistent with other validity requirements for "pointer-like" things such as
fn;
The rules also imply that...
- valid (i.e., compiler-generated) vtables are only required for a
*dynpointer when- the
*dynpointer is upcast (or invoke methods); - or, when the
*dynpointer is released to arbitrary code, because that code may upcast (or invoke methods).- By implication, extending
std::ptr::nullto permitT: ?Sizedwould not be safe.
- By implication, extending
- the
Possible future changes
It may be possible to weaken the validity or safety invariants later, but we risk finding that people have written unsafe code that relies on them. For example, people could build data structures using unions that assume that the vtable pointer is non-null. If we then later permitted a null value, this code could create UB. This is particularly problematic for changes to the safety invariant, since the assumption is that one can take a value which meets the safety invariant and give it to arbitrary code without creating UB (if the value only meets the validity invariant, and not the safety invariant, then you are supposed to audit and control all the code which uses that value, so this is less of a problem).
Prior links
- Dyn safety write-up, which includes links to prior write-ups
- Exhaustive set of validity invariants considered
Activity
nikomatsakis commentedon Sep 2, 2022
nikomatsakis commentedon Sep 2, 2022
also cc @crlf0710, the person driving this initiative
nikomatsakis commentedon Sep 3, 2022
OK, I've adjusted the proposal slightly so that the validity invariant is now word-aligned, non-null. This is consistent with other pointer-like values and offers us more room for future change if needed.
As such, I am ready to ...
@rfcbot fcp merge
rfcbot commentedon Sep 3, 2022
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members:
No concerns currently listed.
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
RalfJung commentedon Sep 3, 2022
I am not sure if that is true -- there could be code relying on this safety invariant. Extreme case -- this fn is sound under the rules as specified above:
This relates to that question about "type system invariants with wiggle room" that you keep getting back to. :)
CAD97 commentedon Sep 3, 2022
I just want to note that with
feature(ptr_metadata), the safety and validity invariants of the metadata part of*const dyn Traitmust be the same as the safety and validity invariants of<fn std::ptr::metadata::<dyn Trait>>::Output. (This type is currently spelledstd::ptr::DynMetadata<dyn Trait>.) This is a necessary forstd::ptr::metadata,<ptr>::to_raw_parts, andstd::ptr::from_raw_partsto be safe.It is perhaps also worth drawing analogy to slices, which stably have a metadata of unburdened
usize, exposed viastd::ptr::slice_from_raw_partsand normal casts1. This prevents a safefn(*const [T]) -> Layoutbecause even without thesize <= isize::MAXrule, the size calculation ofsize_of::<T>() * lencan overflow. (A checked operation can in theory be provided.) Given it's possible/safe to cast from*const [()]to*const [T](they have the same ptr metadata type), this is necessary but unfortunate in hindsight2.With
*const dyn Traitwe have a bit more typesafe wiggle room, as*const dyn Trait1and*const dyn Trait2have distinct metadata types (ascasting between them is forbidden). I personally agree that the invariants of*const dyn Trait's metadata should be validity aligned and nonnull, safety be safe&'static TraitVtable.There's an interesting secondary question, though: do reborrowing rules apply to the pointer's metadata? Given the vtable reference is shared and
'static, the only difference is whether the pointer is markeddereferencable. With aptr_metadatalens,*const Tis(ptr::Opaque, ptr::Metadata<T>), and either semantic is fairly simple to provide; either store&'static TraitVtable<_>(which gets retagged4) orptr::WellFormed<TraitVtable<_>>(which doesn't).Because either semantic can be implemented, this needn't block on figuring out
ptr_metadata, custom unsized pointee kinds, etc. Plus, I think it's a reasonable outcome to recommend authors of custom pointee kinds useptr::WellFormeds rather than&'statics such that pointers to their custom pointee kinds are easier to work with.One final note: it's possible (if annoying) to provide a constant initializer for even a valid vtable pointer. References to
statics are notconst, but AIUI this is because using astaticinconstis not possible, and there's no way to differentiate between a usable and an opaque reference inconst. If (and only if) the vtable reference type is 100% opaque toconst, it'd be possible to represent the vtable reference inconstsymbolically and substitute it out whenstaticaddresses are resolved.There are enough annoyances and roadblocks there that just treating it as a no is reasonable, but it's at least theoretically possible.
Footnotes
While std IIRC does not yet provide any direct way to get the length from an arbitrary
*const [T], you can probably3 do so safely for any nonnull pointer by casting to*const [()]and calling<[()]>::len. ↩Given a time machine, I'd want to have the ptr metadata API from the start and require using that to cast between pointers to
?Sizedtypes. Additionally,ptr::metadata(*const [T])wouldn't returnusizedirectly but instead a type still associated toT, in order to restrict safe*const [T]construction to those which describe valid Rust Allocated Objects. That type (however named) would then offer conversion to and checked fromusize. ↩Caveats around deallocated pointers may apply. If you
alloca ZST and thendeallocit, the pointer may possibly be UB to dereference (this allows reasoning about pointer allocation validity without knowing the pointee size). A weaker form of that is dereferencing a pointer to a real allocation for zero bytes. Personally I think it better/easier to say zero-byte dereferences are always valid (except at null), but there are others who strongly disagree, and I'm not fully aware of the backend/optimizer benefits/concerns around doing polymorphic reasoning which having a special case for zero bytes complicates. ↩Assuming a version of the borrow rules which recurse on fields. ↩
RalfJung commentedon Sep 4, 2022
FWIW, for soundness of those functions, it is enough for the safety invariants to be the same. They don't have to have the same validity invariants.
At least on raw pointers I don't think we should have
dereferenceableor any reborrowing, that would effectively make the validity invariant that this must point to actual memory.So the raw ptr vtable type would be more like
&'static [usize; 0].nikomatsakis commentedon Sep 8, 2022
@RalfJung
Ah, yes, you're correct, but ... hmm. This is equally true of the validity and safety invariants, right? The example you gave was code assuming that the vtable was not NULL, but that's actually guaranteed to be true because of the validity invariant (the safety invariant further refines that to "vtable must be valid" (which we don't precisely define, but anyway).
On the topic of what valid means, I realized that the definition of a valid vtable for a
*const dyn Foovalue shouldn't, I don't think, say that the vtable must be the correct vtable for the "hidden type" of the data pointer, because the data pointer for a*constisn't required to be anything in particular. It's a bit more subtle than that. I don't think we have to establish it at this time though.CAD97 commentedon Sep 8, 2022
To concur and state explicitly: if we want to allow dangling
*const dyn Trait, this is necessary, as the concrete "hidden" pointee type does not exist.(Definitely not an immediate question, but I wonder if we can't make
ptr::null/danglingwork by generating vtables for!; it might even be possible to just have a single shared!vtable (per compilation unit) with enough supertrait pointers to be used as any trait's vtable? Though it might need to have enough dummy function pointers as well if taking the address of the method without calling it is possible.)18 remaining items
rfcbot commentedon Sep 18, 2022
The final comment period, with a disposition to merge, as per the review above, is now complete.
As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.
This will be merged soon.
nikomatsakis commentedon Sep 19, 2022
OK, so this FCP is complete. The question is, what do we do now to close the issue? :)
Option A. Edit the reference or perhaps necronomicon.
Option B. Edit API documentation -- somewhere? I'm not sure if we have an "API page" for "dyn types" in general.
Option C. Open an issue on unsafe-code-guidelines repository and link to this?
@RalfJung or @JakobDegen -- what do people typically do? Have we stabilized decisions about validity/safety conditions before and how did we document them?
The other question was whether to make this a 'strong recommendation' for an opsem-like team, which I would still be ok with. I think the crucial ingredients are...
*const dynto safe code must supply a valid, compiler-supplied vtable (so there is no "general null pointer" that can be released to safe code)Option<*const dyn>-- i.e., we don't want the validity condition to be "anything at all"This leads you to a few options, and the one we ultimately chose was based on Ralf's recommendation beacuse it led to more consistency, so I'd be ok with saying "the opsem team will define the precise conditions, this is our recommendation", though I suppose that's a weakening of what we just FCP'd. :)
RalfJung commentedon Sep 19, 2022
We have #98919 but that doesn't really help here.
https://doc.rust-lang.org/reference/behavior-considered-undefined.html is where we have all validity invariants, so that would be a Reference PR. We don't really have any place to put safety invariants though. Even specifying safety invariants precisely requires something like Iris so it's not like we can even hope to do that anywhere official...
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
Rollup merge of rust-lang#104117 - crlf0710:update_feature_gate, r=ja…
RalfJung commentedon Nov 6, 2023
FWIW #117534 is documenting the safety invariant of
str, and #116677 documents parts of it for references. However, for lack of an official term, they are currently trying to describe the idea of a safety invariant in one sentence rather than using a fixed term.We probably want a fixed term. However I am not sure if "safety invariant" is the best term -- we've been using it for a long time now, but I regret coining it; "library invariant" would be my preferred choice these days. (And "validity invariant" -> "language invariant". That goes well with "language UB" and "library UB". Though maybe those are not terms we want to use either?) Also I am not sure where to best define such a term. That would have to go together with an introduction to how we think about unsafe code wrapped behind safe abstractions and things like that.
nikomatsakis commentedon Dec 18, 2024
Closing as FCP completed.
cc #134367