You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I believe it would be technically feasible (easy, even) to have unions with at most one unsized variant.
Unsizing should be safe:
T -> dyn U: The vtable contains 2 values relevant for the unsized union:
The alignment: This is stored directly in the vtable of T, accessing it does not access the data pointer
The size: Same.
Note that drop_in_place is not relevant because unions do not drop their contents.
[T; N] -> [T]: The metadata consists of the array length which is statically known and does not require accessing the variant.
@eddyb: In the last paragraph here you wrote that there is a problem with unsized unions but I haven't been able to figure it out. Maybe you were talking about unions with more than one unsized variant?
The problem is that the meta-data in the fat pointer won't always be valid, i.e. what should this do?
unionFoo<T: ?Sized>{value:T,uninit:(),}let foo = Foo::<i32>{uninit:()};let foo_ref = &foo as&Foo<dynAny>;// the fat pointer meta-data isn't valid for the data actually stored
Also, if you ban accessing the data, it makes it harder to implement something like Custom DSTs. Custom DSTs are important because they would allow CStr to be a thin pointer. (Is unsized unions that important? I don't know, maybe, maybe not)
The problem is that the meta-data in the fat pointer won't always be valid
Yes, and? Afaik, the only parts of the metadata that MUST be valid are size and alignment so that the layout can be computed at runtime. The other parts (drop_in_place and trait methods) cannot be accessed without first accessing the unsized field. Accessing the wrong field is already undefined behavior so that the metadata being invalid does not add any additional unsafety.
Any map T -> dyn U for T: !Sized sounds quite extreme for Rust. It'd require Box, et al. magically copy the vtable somewhere, not sure that's zero cost, and really not sure what all that breaks.
I think previous discussions indicate that truly unsized types should panic whenever accessing their size or alignment, so align_of_val and size_of_val should never access their data pointer.
If I understood, Rust could add some TrueCStr (name?) which yields thin pointers, and maybe even makes align_of_val work, but for which size_of_val panics. It'd still have a method like strlen and a conversion into slices.
Now, given &Example (which I suppose is a wide pointer), how do you determine the size?
Note that not even enums can have unsized fields currently, and that seems like a strictly simpler problem since for enums we know which variant they are in. So I think we should specify, implement, and gather experience with unsized enums before venturing into the land of unsized unions.
Now, given &Example (which I suppose is a wide pointer), how do you determine the size?
How do you construct &Example? Presumably with transmute? In this case you would either transmute it from &str or from [&String, 0]. So the size would be max(metadata, sizeof(String)). Or more generally for [T] fields:
max(metadata * sizeof(T), sizeof(union without the unsized field))
(Rounded up to the correct alignment.)
Automatic unsizing of [T; N] fields would of course set the metadata to the correct value just like unsizing &[T; N] would.
Note that not even enums can have unsized fields currently, and that seems like a strictly simpler problem since for enums we know which variant they are in.
I think enums are significantly more complicated in this case. The &Example above can be constructed via transmute. If it were an enum, this would not be possible.
max(metadata * sizeof(T), sizeof(union without the unsized field))
So you are suggesting that the metadata is 0 when the String variant was used to create this enum? Leaving aside that it is completely unclear how to do the unsizing coercion, what do you propose to do if the unsized field is of type dyn Trait? (Note that the vtable metadata field is currently marked nonnull.)
So you are suggesting that the metadata is 0 when the String variant was used to create this enum?
We're talking about unions and not enums. The variant does not matter for unsizing and the compiler does not have to know which variant was last written to.
Leaving aside that it is completely unclear how to do the unsizing coercion
what do you propose to do if the unsized field is of type dyn Trait? (Note that the vtable metadata field is currently marked nonnull.)
X<u8> unsizes to X<dyn Clone> with the metadata of u8 as Clone.
Accessing the wrong field is simply undefined behavior. E.g.
let x:X<u8> = X{v:ManuallyDrop::new(String::new())};let x:&X<dynClone> = &x;unsafe{
x.t.clone();// UB}
(The current rules for sized fields are a bit more relaxed but for unsized fields we can simply make up the new rule that accessing an "inactive" unsized field is unconditionally UB.)
Btw, since you mentioned that earlier -- note that unsizing-by-transmute is incorrect since the layout of wide pointers is not specified.
X<[u8; 22]> unsizes to X<[u8]> with metadata 22.
Oh, I see. That makes sense. Basically where struct with unsized field adds up all the static field sizes with the one of the unsized field, union takes a max instead. Unsizing will always be able to determine that size without looking at the data stored in the union because pre-unsizing, it is all present in the type. Initializing a X<[u8; 22]> requires specifying the 22 even if we initialize it in the String field -- this is the bit that I missed before.
This also works for dyn Trait.
When nesting union and struct, this can lead to more complicated code for size_of_val (a sequence of additions and max), but it is still possible to implement this in time linear to the depth of the type (so far it's constant, though). Maybe there is a more clever compliation strategy, but that's not something I am fundamentally worried about.
(The current rules for sized fields are a bit more relaxed but for unsized fields we can simply make up the new rule that accessing an "inactive" unsized field is unconditionally UB.)
There is no such thing as an "active" or "inactive" union field, and I think adding that notion would be a mistake. (It would also be quite complicated.) It also seems unnecessary -- your code is equivalent to reading x.t from the original sized x, i.e., it can still be explained by "union field accesses are transmutes".
The one issue that remains is the same as what I mentioned in rust-lang/rust#80158: this proposal relies on determining the size of Box<T: !Sized>without accessing the pointee, which is in conflict with very general proposals for custom DST. That conflict could be resolvable in principle, but will lead to a lot of friction nevertheless.
rc::Weak already calls align_of_val_raw on invalid pointers.
The code is careful to spell out that it is valid only when the "unsized tail" of T is a slices or a trait object. AFAIK the public API of Rc makes it impossible to create other Rc<T> for unsized T, even if the respective unsizing coercions were added. Do you see a loophole in that approach?
If I understand you correctly, you're saying that Weak<T> can never be safely converted to Weak<U> where U is not sized but neither a slice or a trait.
The only other unsized types I'm aware of are extern types and I'm not aware of any way to unsize any type to an extern type. Therefore this statement would be trivially true in the current language and also hold for unions.
But even if there were special logic (maybe some marker trait?) that allowed Weak to coerce to slices or traits but not to other unsized types, could the same logic not also be applied to unions without extending the language?
The discussion above, as I understand it, is about figuring out to what extend ThinDst is already broken given what is currently stable.
Yes.
There is a potential fix here by requiring that for custom DSTs, determining the size must be possible even after the drop glue ran (but before the underlying memory got deallocated). However, that still sounds quite awful and would seriously restrict which custom DSTs are possible.
Code like ZeroOnDrop already exists in the wild. If Display were replaced by a ThinDst, any size information it might want to access would already have been zeroed out at the end of f.
That is another interesting idea... custom DSTs could be required to provide a function that extracts all the data now such that, later, this data plus the ptr metadata is enough to reconstruct size and alignment. This extra data would be () for currently existing DST (since ptr metadata is enough) but thin trait objects you put size+alignment into that data (or just the vtable ptr), and RcBox/ArcBox could store that extra data.
I think there are two ideas here.
dyn Dst pointers store the extra data side-by-side with the pointer. This means that size+alignment can be determined without dereferencing the data pointer. Basically how it works today. But this would probably make thin DSTs impossible and dyn Dst pointers might be very large for some DSTs: data+vtable+size+alignment.
It's up to the container to store size+alignment if it needs to so that it can later be deallocated. The problem with this is that RcBox<T> is usually constructed with a sized <T>. To accommodate T unsizing to a custom DST, it would have to reserve an additional 16 bytes in its allocation to be able to store the size+alignment information. If it's not possible to determine at compile time if a type T might unsize to a custom DST, this would add a 16 byte overhead to all RcBoxs.
However... Box isn't actually dropping its content before determining size+alignment, so there's no reason here why Box would have to do this. So something is still wrong here.
By that comment I meant that, if we are willing to pay a blanked 16 byte overhead for all RcBox allocations only to support custom DSTs, then why not do the same with Box to support Box<Union<dyn Dst>>. Box then stores this additional data in its constructor.
Wouldn't this mean ManuallyDrop::take becomes unsound, since it leaves behind a "moved-out-of" unsized thing of which the size still needs to be determinable?
If size+alignment are always calculated by container constructors which work only with sized types by design, then there is no problem because the vtable is not needed for size calculations.
If size+alignment calculations use the "access after drop/before deallocate" idea you suggested, then ManuallyDrop::take should be fine because it leaves the memory unchanged as I understand it. But the ZeroOnDrop example breaks this either way.
Code like ZeroOnDrop already exists in the wild. If Display were replaced by a ThinDst, any size information it might want to access would already have been zeroed out at the end of f.
Note: that code as written is unsound because zeroing references/other nonnull pointers is undefined behavior. The fix is to use MaybeUninit, which conveniently doesn't allow unsized types. So that doesn't apply to this discussion.
By that comment I meant that, if we are willing to pay a blanked 16 byte overhead for all RcBox allocations only to support custom DSTs, then why not do the same with Box to support Box<Union>. Box then stores this additional data in its constructor.
Because the Box isn't dropping dyn Dst, it's dropping Union. So it would be on the author of Union to correctly drop dyn Dst.
Note: that code as written is unsound because zeroing references/other nonnull pointers is undefined behavior.
I disagree. Invalidating the state of ManuallyDrop is a supported operation as long as the ManuallyDrop is not accessed afterwards. See ManuallyDrop::drop. Overwriting it is no different since it's not accessed afterwards.
Because the Box isn't dropping dyn Dst, it's dropping Union. So it would be on the author of Union to correctly drop dyn Dst.
It's not about dropping the contents. It's about calculating the size and alignment of the contents.
I disagree. Invalidating the state of ManuallyDrop is a supported operation as long as the ManuallyDrop is not accessed afterwards. See ManuallyDrop::drop. Overwriting it is no different since it's not accessed afterwards.
From the ManuallyDrop docs
ManuallyDrop is subject to the same layout optimizations as T. As a consequence, it has no effect on the assumptions that the compiler makes about its contents.
It's unsound to zero &T, so it's unsound to zero ManuallyDrop<&T>
Is there some good reason for T -> dyn Trait conversions with T: !Sized?
I think there are two ideas here.
..
It's clear both 1 and 2 are non-zero cost for almost all use cases, for what amounts to an extraordinarily niche feature.
Instead metadata should be specified explicitly by pointer types, so either make traits specify their metadata perhaps via trait Foo : Pointee<Meta = MyMeta> {} using #2984 syntax, or else specify in the dyn type so dyn<MyMeta> Trait or dyn Trait+Pointee<Meta = MyMeta> again using #2984 syntax.
In this, one could not ever convert a &dyn<MyMeta> Trait to a &dyn Trait, but perhaps the reverse makes sense sometimes.
As an aside, if one wants more complex metadata then really types like struct Foo([X],[Y]); look much more useful than dyn whatever, meaning &Foo requires one data pointer and two lengths.
By that comment I meant that, if we are willing to pay a blanked 16 byte overhead for all RcBox allocations only to support custom DSTs, then why not do the same with Box to support Box<Union>. Box then stores this additional data in its constructor.
I understand. What I am trying to figure out is which protocol Box would be breaking if it didn't do that. Clearly, a Box that doesn't do this is wrong under Custom-DST + Unsized-Unions. But which safety condition is being violated where?
But which safety condition is being violated where?
That particular statement was not intended to show that there is already a problem with DSTs. (I think that's what you mean by your question which I didn't understand the first time.)
Noting this here before I forget: even without my additional as_ptr/into_raw/from_raw support for Weak<T: ?Sized>, it was already possible to create and drop the last dangling Weak because of unsize coercion:
See rust-lang/rust#80407 for follow-up on the dangling unsized Weak issue. With cleverer implementations for our refcounted pointers, they don't require align_of_val_raw at all, so shouldn't be used as rationalization for "it's already possible." See the linked issue for more context and/or further discussion.
That particular statement was not intended to show that there is already a problem with DSTs. (I think that's what you mean by your question which I didn't understand the first time.)
Right, but in a world with your unsized unions and custom thin DST, Box is unsound. So where's the bug, where's the unsoundness coming from, which condition is being violated where?
I'm sorry but it looks like I still don't understand your question. Are you asking where the unsoundness with Box+thin Dst+unsized unions is? You've already spelled it out in this issue. Are you asking where an existing unsoundness with Box+thin Dst is? Like I said, this particular sentence had nothing to with showing that there is existing unsoundness. That sentence was about showing a way in which Box+thin Dst+unsized unions could be made sound.
Are you asking where the unsoundness with Box+thin Dst+unsized unions is?
Yes.
You've already spelled it out in this issue.
I've spelled out what goes wrong, operationally. But I'd like to understand better what happens. Since something goes wrong, clearly a proof of soundness for this system will fail. What I haven't understood yet is where it will fail.
It's one thing to say "having Box store size and alignment up-front fixes this", but that still doesn't say which contract would be violated by not doing so.
So here's my rambling thoughts on this, typing them out pretty much as they come to my mind:
Box only calls size_of_val on live values. I can't see any way that Box is violating the preconditions of anything here.
Thin DST themselves are also fine, I think.
So the real culprit, then, are unsized unions.
Unsized unions essentially mean we have to remove size_of_val and align_of_val from the language -- they become unusable. Literally the only thing Box is doing wrong here is calling these functions. So really the unsoundness is in having size_of_val + thin DST + unsized unions.
So if we were to add unsized unions, then we'd have to accept that size_of_val will not be callable in situations where there might be a thin DST. Notwithstanding the align_of_val_raw issue in Rc that you uncovered, this seems like a big leap.
Have unsized unions at least for those DSTs that can determine their layout without accessing the data pointer.
Not make implementing thin DSTs harder.
Not have size_of_val(t) panic just because t contains a union with a thin DST field.
Not add any new type bounds that mean "this is a DST whose layout can be determined without accessing the data pointer."
What about stopping the problem at Unsize?
/// - `[T; N]` is `Unsize<[T]>`/// - `T` is `Unsize<dyn Trait>` when `T: Trait`/// - `Foo<..., T, ...>` is `Unsize<Foo<..., U, ...>>` if:/// - `T: Unsize<U>`/// - Foo is a struct/// - Only the last field of `Foo` has a type involving `T`/// - `T` is not part of the type of any other fields/// - `Bar<T>: Unsize<Bar<U>>`, if the last field of `Foo` has type `Bar<T>`
All implementations of Unsize are provided by the compiler and, of course, there are currently no implementations specified for unions. We could have the following rules for unions:
Foo<..., T, ...> is Unsize<Foo<..., U, ...>> if:
T: Unsize<U>
Foo is a union
At most one variant of Foo has a type involving T
T is not part of the type of any other variant
Bar<T>: Unsize<Bar<U>>, if the variant of Foo involving T has type Bar<T>
Determining the size_of_val/align_of_val of &Bar<U> does not require accessing the data pointer of &Bar<U>
The last point being the difference to struct. This property holds for traits and slices and custom DSTs could opt into this property by implementing some unsafe trait.
Note that we would allow
unionU<T: ?Sized>{t:ManuallyDrop<T>,u:(),}
to compile even though the behavior would be undefined if you could call size_of_val on a &U<ThinDst>. However, there would be no safe way to construct such a U because U<T> does not implement Unsize<U<ThinDst>>.
So basically, "fat DST" would work in unions but thin DST would not, by controlling which unsizing coercions the compiler permits?
Yeah, something like this could work. There might be semver concerns here since now it matters whether an unsized type parameter is used below a union or not -- but maybe that is acceptable.
Activity
RustyYato commentedon Dec 19, 2020
The problem is that the meta-data in the fat pointer won't always be valid, i.e. what should this do?
Also, if you ban accessing the data, it makes it harder to implement something like Custom DSTs. Custom DSTs are important because they would allow
CStrto be a thin pointer. (Is unsized unions that important? I don't know, maybe, maybe not)mahkoh commentedon Dec 19, 2020
Yes, and? Afaik, the only parts of the metadata that MUST be valid are size and alignment so that the layout can be computed at runtime. The other parts (drop_in_place and trait methods) cannot be accessed without first accessing the unsized field. Accessing the wrong field is already undefined behavior so that the metadata being invalid does not add any additional unsafety.
Lokathor commentedon Dec 19, 2020
I'm not clear on why CStr can't already be a thin pointer.
burdges commentedon Dec 19, 2020
Any map
T -> dyn UforT: !Sizedsounds quite extreme for Rust. It'd requireBox, et al. magically copy the vtable somewhere, not sure that's zero cost, and really not sure what all that breaks.I think previous discussions indicate that truly unsized types should panic whenever accessing their size or alignment, so
align_of_valandsize_of_valshould never access their data pointer.If I understood, Rust could add some
TrueCStr(name?) which yields thin pointers, and maybe even makesalign_of_valwork, but for whichsize_of_valpanics. It'd still have a method like strlen and a conversion into slices.kennytm commentedon Dec 20, 2020
that would make
Box<CStr>crashmahkoh commentedon Dec 20, 2020
Please let's stop this academic discussion around CStr. CStr will never be changed simply because it would
People who actually want a thin pointer to null terminated strings have written such a thing themselves years ago and have no need for a thin CStr.
Feel free to talk about new forms of metadata in general.
RalfJung commentedon Dec 20, 2020
I don't know how to even implement this. Imagine
Now, given
&Example(which I suppose is a wide pointer), how do you determine the size?Note that not even enums can have unsized fields currently, and that seems like a strictly simpler problem since for enums we know which variant they are in. So I think we should specify, implement, and gather experience with unsized enums before venturing into the land of unsized unions.
mahkoh commentedon Dec 20, 2020
How do you construct
&Example? Presumably with transmute? In this case you would either transmute it from&stror from[&String, 0]. So the size would bemax(metadata, sizeof(String)). Or more generally for[T]fields:(Rounded up to the correct alignment.)
Automatic unsizing of
[T; N]fields would of course set the metadata to the correct value just like unsizing&[T; N]would.I think enums are significantly more complicated in this case. The
&Exampleabove can be constructed viatransmute. If it were an enum, this would not be possible.RalfJung commentedon Dec 21, 2020
That doesn't matter for my question. No matter how it is constructed, you have to be able compile code like
in a uniform way, without knowing how the arguments got created.
RalfJung commentedon Dec 21, 2020
So you are suggesting that the metadata is
0when theStringvariant was used to create this enum? Leaving aside that it is completely unclear how to do the unsizing coercion, what do you propose to do if the unsized field is of typedyn Trait? (Note that the vtable metadata field is currently markednonnull.)mahkoh commentedon Dec 21, 2020
We're talking about unions and not enums. The variant does not matter for unsizing and the compiler does not have to know which variant was last written to.
Which part is unclear? Given
X<[u8; 22]>unsizes toX<[u8]>with metadata22.X<u8>unsizes toX<dyn Clone>with the metadata ofu8 as Clone.Accessing the wrong field is simply undefined behavior. E.g.
(The current rules for sized fields are a bit more relaxed but for unsized fields we can simply make up the new rule that accessing an "inactive" unsized field is unconditionally UB.)
RalfJung commentedon Dec 21, 2020
Btw, since you mentioned that earlier -- note that unsizing-by-
transmuteis incorrect since the layout of wide pointers is not specified.Oh, I see. That makes sense. Basically where
structwith unsized field adds up all the static field sizes with the one of the unsized field,uniontakes amaxinstead. Unsizing will always be able to determine that size without looking at the data stored in the union because pre-unsizing, it is all present in the type. Initializing aX<[u8; 22]>requires specifying the22even if we initialize it in theStringfield -- this is the bit that I missed before.This also works for
dyn Trait.When nesting
unionandstruct, this can lead to more complicated code forsize_of_val(a sequence of additions andmax), but it is still possible to implement this in time linear to the depth of the type (so far it's constant, though). Maybe there is a more clever compliation strategy, but that's not something I am fundamentally worried about.There is no such thing as an "active" or "inactive" union field, and I think adding that notion would be a mistake. (It would also be quite complicated.) It also seems unnecessary -- your code is equivalent to reading
x.tfrom the original sizedx, i.e., it can still be explained by "union field accesses are transmutes".The one issue that remains is the same as what I mentioned in rust-lang/rust#80158: this proposal relies on determining the size of
Box<T: !Sized>without accessing the pointee, which is in conflict with very general proposals for custom DST. That conflict could be resolvable in principle, but will lead to a lot of friction nevertheless.mahkoh commentedon Dec 21, 2020
rc::Weakalready callsalign_of_val_rawon invalid pointers.RalfJung commentedon Dec 22, 2020
The code is careful to spell out that it is valid only when the "unsized tail" of
Tis a slices or a trait object. AFAIK the public API ofRcmakes it impossible to create otherRc<T>for unsizedT, even if the respective unsizing coercions were added. Do you see a loophole in that approach?mahkoh commentedon Dec 22, 2020
The code I was thinking of is
https://play.rust-lang.org/?version=beta&mode=debug&edition=2018&gist=62b9754a432976a2646cf30812e60913
If I understand you correctly, you're saying that
Weak<T>can never be safely converted toWeak<U>whereUis not sized but neither a slice or a trait.The only other unsized types I'm aware of are extern types and I'm not aware of any way to unsize any type to an extern type. Therefore this statement would be trivially true in the current language and also hold for unions.
But even if there were special logic (maybe some marker trait?) that allowed
Weakto coerce to slices or traits but not to other unsized types, could the same logic not also be applied to unions without extending the language?6 remaining items
mahkoh commentedon Dec 26, 2020
Yes.
I do not think that is possible. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f6532229a7706836246360fb1af18db3
Code like
ZeroOnDropalready exists in the wild. IfDisplaywere replaced by aThinDst, any size information it might want to access would already have been zeroed out at the end off.I think there are two ideas here.
dyn Dstpointers store the extra data side-by-side with the pointer. This means that size+alignment can be determined without dereferencing the data pointer. Basically how it works today. But this would probably make thin DSTs impossible anddyn Dstpointers might be very large for some DSTs: data+vtable+size+alignment.RcBox<T>is usually constructed with a sized<T>. To accommodateTunsizing to a custom DST, it would have to reserve an additional 16 bytes in its allocation to be able to store the size+alignment information. If it's not possible to determine at compile time if a typeTmight unsize to a custom DST, this would add a 16 byte overhead to allRcBoxs.By that comment I meant that, if we are willing to pay a blanked 16 byte overhead for all
RcBoxallocations only to support custom DSTs, then why not do the same with Box to supportBox<Union<dyn Dst>>.Boxthen stores this additional data in its constructor.If size+alignment are always calculated by container constructors which work only with sized types by design, then there is no problem because the vtable is not needed for size calculations.
If size+alignment calculations use the "access after drop/before deallocate" idea you suggested, then
ManuallyDrop::takeshould be fine because it leaves the memory unchanged as I understand it. But theZeroOnDropexample breaks this either way.RustyYato commentedon Dec 26, 2020
@mahkoh
Note: that code as written is unsound because zeroing references/other nonnull pointers is undefined behavior. The fix is to use
MaybeUninit, which conveniently doesn't allow unsized types. So that doesn't apply to this discussion.Because the
Boxisn't droppingdyn Dst, it's droppingUnion. So it would be on the author ofUnionto correctly dropdyn Dst.mahkoh commentedon Dec 26, 2020
I disagree. Invalidating the state of ManuallyDrop is a supported operation as long as the ManuallyDrop is not accessed afterwards. See ManuallyDrop::drop. Overwriting it is no different since it's not accessed afterwards.
It's not about dropping the contents. It's about calculating the size and alignment of the contents.
RustyYato commentedon Dec 26, 2020
From the
ManuallyDropdocsIt's unsound to zero
&T, so it's unsound to zeroManuallyDrop<&T>Also see: rust-lang/unsafe-code-guidelines#245
Right, I misunderstood
burdges commentedon Dec 26, 2020
Is there some good reason for
T -> dyn Traitconversions withT: !Sized?It's clear both 1 and 2 are non-zero cost for almost all use cases, for what amounts to an extraordinarily niche feature.
Instead metadata should be specified explicitly by pointer types, so either make traits specify their metadata perhaps via
trait Foo : Pointee<Meta = MyMeta> {}using #2984 syntax, or else specify in thedyntype sodyn<MyMeta> Traitordyn Trait+Pointee<Meta = MyMeta>again using #2984 syntax.In this, one could not ever convert a
&dyn<MyMeta> Traitto a&dyn Trait, but perhaps the reverse makes sense sometimes.As an aside, if one wants more complex metadata then really types like
struct Foo([X],[Y]);look much more useful thandynwhatever, meaning&Foorequires one data pointer and two lengths.mahkoh commentedon Dec 26, 2020
Yes it looks like I was wrong. I'll see if I can come up with another example.
RalfJung commentedon Dec 26, 2020
I understand. What I am trying to figure out is which protocol
Boxwould be breaking if it didn't do that. Clearly, aBoxthat doesn't do this is wrong under Custom-DST + Unsized-Unions. But which safety condition is being violated where?mahkoh commentedon Dec 26, 2020
That particular statement was not intended to show that there is already a problem with DSTs. (I think that's what you mean by your question which I didn't understand the first time.)
CAD97 commentedon Dec 27, 2020
Noting this here before I forget: even without my additional
as_ptr/into_raw/from_rawsupport forWeak<T: ?Sized>, it was already possible to create and drop the last danglingWeakbecause of unsize coercion:I think this means that my stabilization didn't add any new requirements to the impl, as they already existed in
Weak's drop implementation.EDIT: wait no dangling weak don't have to deallocate anything what was I thinking whoops
CAD97 commentedon Dec 27, 2020
See rust-lang/rust#80407 for follow-up on the dangling unsized
Weakissue. With cleverer implementations for our refcounted pointers, they don't requirealign_of_val_rawat all, so shouldn't be used as rationalization for "it's already possible." See the linked issue for more context and/or further discussion.RalfJung commentedon Dec 27, 2020
Right, but in a world with your unsized unions and custom thin DST,
Boxis unsound. So where's the bug, where's the unsoundness coming from, which condition is being violated where?mahkoh commentedon Dec 27, 2020
I'm sorry but it looks like I still don't understand your question. Are you asking where the unsoundness with Box+thin Dst+unsized unions is? You've already spelled it out in this issue. Are you asking where an existing unsoundness with Box+thin Dst is? Like I said, this particular sentence had nothing to with showing that there is existing unsoundness. That sentence was about showing a way in which Box+thin Dst+unsized unions could be made sound.
RalfJung commentedon Dec 27, 2020
Yes.
I've spelled out what goes wrong, operationally. But I'd like to understand better what happens. Since something goes wrong, clearly a proof of soundness for this system will fail. What I haven't understood yet is where it will fail.
It's one thing to say "having
Boxstore size and alignment up-front fixes this", but that still doesn't say which contract would be violated by not doing so.So here's my rambling thoughts on this, typing them out pretty much as they come to my mind:
Boxonly callssize_of_valon live values. I can't see any way thatBoxis violating the preconditions of anything here.Thin DST themselves are also fine, I think.
So the real culprit, then, are unsized unions.
Unsized unions essentially mean we have to remove
size_of_valandalign_of_valfrom the language -- they become unusable. Literally the only thingBoxis doing wrong here is calling these functions. So really the unsoundness is in havingsize_of_val+ thin DST + unsized unions.So if we were to add unsized unions, then we'd have to accept that
size_of_valwill not be callable in situations where there might be a thin DST. Notwithstanding thealign_of_val_rawissue inRcthat you uncovered, this seems like a big leap.mahkoh commentedon Dec 28, 2020
You make a compelling point.
We want
size_of_val(t)panic just becausetcontains a union with a thin DST field.What about stopping the problem at
Unsize?All implementations of
Unsizeare provided by the compiler and, of course, there are currently no implementations specified for unions. We could have the following rules for unions:Foo<..., T, ...>isUnsize<Foo<..., U, ...>>if:T: Unsize<U>Foohas a type involvingTTis not part of the type of any other variantBar<T>: Unsize<Bar<U>>, if the variant ofFooinvolvingThas typeBar<T>&Bar<U>does not require accessing the data pointer of&Bar<U>The last point being the difference to struct. This property holds for traits and slices and custom DSTs could opt into this property by implementing some unsafe trait.
Note that we would allow
to compile even though the behavior would be undefined if you could call
size_of_valon a&U<ThinDst>. However, there would be no safe way to construct such aUbecauseU<T>does not implementUnsize<U<ThinDst>>.RalfJung commentedon Dec 30, 2020
So basically, "fat DST" would work in unions but thin DST would not, by controlling which unsizing coercions the compiler permits?
Yeah, something like this could work. There might be semver concerns here since now it matters whether an unsized type parameter is used below a union or not -- but maybe that is acceptable.