Introduce doc(hidden) derive(Project) - #3492
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## Gd2bc8560585e8f472223acf71b96e0f71b2d00c2 #3492 +/- ##
=============================================================================
- Coverage 91.87% 91.78% -0.09%
=============================================================================
Files 20 20
Lines 6101 6121 +20
=============================================================================
+ Hits 5605 5618 +13
- Misses 496 503 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
35b52b5 to
ba8ac3a
Compare
| // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT | ||
| // |
There was a problem hiding this comment.
Needs to be the standard copyright header.
There was a problem hiding this comment.
Still need to remove this:
| // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT | |
| // |
| // This casts the enum variant to its discriminant, and then | ||
| // converts the discriminant to the target integral type via a | ||
| // numeric cast [1]. | ||
| // | ||
| // Because these are the same size, this is defined to be a no-op | ||
| // and therefore is a lossless conversion [2]. | ||
| // | ||
| // [1] Per https://doc.rust-lang.org/1.81.0/reference/expressions/operator-expr.html#enum-cast: | ||
| // | ||
| // Casts an enum to its discriminant. | ||
| // | ||
| // [2] Per https://doc.rust-lang.org/1.81.0/reference/expressions/operator-expr.html#numeric-cast: | ||
| // | ||
| // Casting between two integers of the same size (e.g. i32 -> u32) | ||
| // is a no-op. |
There was a problem hiding this comment.
Technically this should also document that this as conversion is implicitly two-step (ie, enum to discriminant and then discriminant to integer type).
There was a problem hiding this comment.
We were a bit fried when we reviewed this, but we do indeed document this as a two-step conversion!
This casts the enum variant to its discriminant, and then converts the discriminant to the target integral type via a numeric cast.
There was a problem hiding this comment.
Sorry, I meant that we need to cite the Rust documentation that that is the case, not just state it without justification.
| // preserve `Uninit` and `Initialized`, while `Valid` is weakened to | ||
| // `Uninit`. |
There was a problem hiding this comment.
Make sure to update this if we decide not to support mapping Valid to Uninit for unions.
| /// `ProjectionValidity` and `EnumFieldProjection` enforce the remaining | ||
| /// conditions: | ||
| /// | ||
| /// - An uninitialized projection is infallible and remains uninitialized | ||
| /// because `HasField::project` returns a subset of the input bytes and | ||
| /// `Uninit` permits every bit pattern. | ||
| /// - An initialized projection is infallible and remains initialized because | ||
| /// every byte in a subset of a fully initialized byte range is initialized. | ||
| /// - A valid projection is emitted only with a tag check derived from the same | ||
| /// variant as its `VARIANT_ID`. Thus, once `is_projectable` succeeds, the | ||
| /// projected field belongs to the active variant and is valid. This impl | ||
| /// additionally requires `Reference` aliasing; together with the generated | ||
| /// tag type's `Immutable` impl, that prevents the tag from changing during | ||
| /// projection. |
| field_id: Box<Expr>, | ||
| variant_struct_field_index: Index, |
There was a problem hiding this comment.
It feels like these two belong in a single type together. In fact, the only place we call EnumFieldProjection::new (which is the only constructor of this type) is from an enumerated iterator over fields. Thus, we could even introduce an EnumFieldIter which implements Iterator<Item = EnumField>, where EnumField { field_id: Box<Expr>, variant_struct_field_index: Index } (names to be bikeshedded ofc). That would make it clearer that these two fields are coupled rather than orthogonal. It would also give us a natural place to document their relationship.
| data: &DataEnum, | ||
| client: Client, | ||
| projection: &EnumFieldProjection, | ||
| validity: ProjectionValidity, |
There was a problem hiding this comment.
| validity: ProjectionValidity, | |
| input_validity: ProjectionValidity, |
| /// `ProjectField` impls for this enum field. In particular, deriving the | ||
| /// variant ID here ensures that the valid projection's tag check and the | ||
| /// projected field always refer to the same variant. | ||
| fn field_projection(&self, ctx: &Ctx) -> FieldProjection { |
There was a problem hiding this comment.
TODO: Bikeshed better names for either this or for the receiver type or all of the above, considering readability at the use site.
| let projection = projection.field_projection(ctx); | ||
| let FieldProjection { variant_id, field, field_id } = projection; |
| // This casts the enum variant to its discriminant, and then | ||
| // converts the discriminant to the target integral type via a | ||
| // numeric cast [1]. | ||
| // | ||
| // Because these are the same size, this is defined to be a no-op | ||
| // and therefore is a lossless conversion [2]. | ||
| // | ||
| // [1] Per https://doc.rust-lang.org/1.81.0/reference/expressions/operator-expr.html#enum-cast: | ||
| // | ||
| // Casts an enum to its discriminant. | ||
| // | ||
| // [2] Per https://doc.rust-lang.org/1.81.0/reference/expressions/operator-expr.html#numeric-cast: | ||
| // | ||
| // Casting between two integers of the same size (e.g. i32 -> u32) | ||
| // is a no-op. |
There was a problem hiding this comment.
We were a bit fried when we reviewed this, but we do indeed document this as a two-step conversion!
This casts the enum variant to its discriminant, and then converts the discriminant to the target integral type via a numeric cast.
| (Data::Struct(_), validity) | ||
| | (Data::Union(_), validity @ ProjectionValidity::Uninit) | ||
| | (Data::Union(_), validity @ ProjectionValidity::Initialized) => validity, | ||
| (Data::Union(_), ProjectionValidity::Valid) => ProjectionValidity::Uninit, |
There was a problem hiding this comment.
This doesn't have the same soundness issue per se, but it is basically the same as the soundness issue we're grappling with in rust-lang/reference#2340.
If the goal of our projection system is to provide all the safe projections there are, we should provide this projection, but doing so will conflict with also providing derive(IntoBytes) on unions.
This makes me worried that rust-lang/reference#2340 is heavy-handed; this dilemma could alternatively be resolved by making IntoBytes and unsafe derive (if such a thing existed) on unions. We want unsafe derives for other reasons, and perhaps that's where we should be throwing our weight.
f14a0cc to
f2265d4
Compare
There was a problem hiding this comment.
Want to highlight this: It's now the case that the only occurrences of unsafe in derive(TryFromBytes) on enums are unsafe impls, and calls to addr_of!
df8080c to
d32515a
Compare
derive(Project)doc(hidden) derive(Project)
d32515a to
3501462
Compare
This factors out the `derive(TryFromBytes)` into `project.rs`, and exposes this machinery as a doc-hidden `derive(Project)`. The implementation of `is_bit_valid` on enums is now fully safe, and a soundness issue of mutable enum tag projection is resolved by permitting only immutable tag projection. gherrit-pr-id: Gcc56a15ce65387a017464947e8c2f25dc9e8c737
3501462 to
19a6a1b
Compare
644b98b to
a175cac
Compare
| // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT | ||
| // |
There was a problem hiding this comment.
Still need to remove this prefix:
| // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT | |
| // |
There was a problem hiding this comment.
Didn't we add those intentionally for the linux folks? We have this prefix everywhere: https://github.com/search?q=repo%3Agoogle%2Fzerocopy+SPDX-License-Identifier%3A+BSD-2-Clause+OR+Apache-2.0+OR+MIT&type=code
| // This casts the enum variant to its discriminant, and then | ||
| // converts the discriminant to the target integral type via a | ||
| // numeric cast [1]. | ||
| // | ||
| // Because these are the same size, this is defined to be a no-op | ||
| // and therefore is a lossless conversion [2]. | ||
| // | ||
| // [1] Per https://doc.rust-lang.org/1.81.0/reference/expressions/operator-expr.html#enum-cast: | ||
| // | ||
| // Casts an enum to its discriminant. | ||
| // | ||
| // [2] Per https://doc.rust-lang.org/1.81.0/reference/expressions/operator-expr.html#numeric-cast: | ||
| // | ||
| // Casting between two integers of the same size (e.g. i32 -> u32) | ||
| // is a no-op. |
There was a problem hiding this comment.
Sorry, I meant that we need to cite the Rust documentation that that is the case, not just state it without justification.
| (Data::Struct(_), validity) | ||
| | (Data::Union(_), validity @ ProjectionValidity::Uninit) | ||
| | (Data::Union(_), validity @ ProjectionValidity::Initialized) => validity, | ||
| (Data::Union(_), ProjectionValidity::Valid) => ProjectionValidity::Uninit, |
There was a problem hiding this comment.
So to clarify: since unions have no bit validity requirements from a language standpoint, then both the source and destination validities permit uninit bytes, and so this neither expands nor shrinks the validity. If instead we care about library safety, then it's a question of the "default" safety invariant, which is an open question.
However, I also think there's a soundness hole here regardless: This currently admits Initialized -> Uninit which is invalid regardless of union validity.
Conversation continued here.
| // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT | ||
| // |
There was a problem hiding this comment.
Still need to remove this:
| // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT | |
| // |
| // safe-transmute reasons, we can add a way to opt-in to that, | ||
| // e.g. via an attribute on the union or on the field that | ||
| // should define the niche of the union. | ||
| (Data::Union(_), Validity::Valid) => Validity::Uninit, |
There was a problem hiding this comment.
So to clarify: since unions have no bit validity requirements from a language standpoint, then both the source and destination validities permit uninit bytes, and so this neither expands nor shrinks the validity. If instead we care about library safety, then it's a question of the "default" safety invariant, which is an open question.
I don't think there's any open question here. As I outlined here, it's already the case that anyone in the ecosystem could provide an abstraction like the one here and it would be sound, because the language enforces using unsafe on union field accesses and a discharge of the validity proof.
If the user wishes to have a compiler-enforced safety invariant, they will be able to do so by marking their field as unsafe. As a rule, we won't provide safe projections for unsafe fields.
However, I also think there's a soundness hole here regardless: This currently admits
Initialized->Uninitwhich is invalid regardless of union validity.
How so? For unions, this match maps Uninit to Uninit, Initialized to Initialized, and Valid to Uninitialized, but not Valid to Initialized.
This factors out the
derive(TryFromBytes)intoproject.rs, andexposes this machinery as a doc-hidden
derive(Project). Theimplementation of
is_bit_validon enums is now fully safe, and asoundness issue of mutable enum tag projection is resolved by
permitting only immutable tag projection.
doc(hidden)derive(Project)#3492Clientparameter to projection traits #3480Latest Update: v11 — Compare vs v10
📚 Full Patch History
Links show the diff between the row version and the column version.
⬇️ Download this PR
Branch
git fetch origin refs/heads/Gcc56a15ce65387a017464947e8c2f25dc9e8c737 && git checkout -b pr-Gcc56a15ce65387a017464947e8c2f25dc9e8c737 FETCH_HEADCheckout
git fetch origin refs/heads/Gcc56a15ce65387a017464947e8c2f25dc9e8c737 && git checkout FETCH_HEADCherry Pick
git fetch origin refs/heads/Gcc56a15ce65387a017464947e8c2f25dc9e8c737 && git cherry-pick FETCH_HEADPull
Stacked PRs enabled by GHerrit.