-
Couldn't load subscription status.
- Fork 128
[WIP] Try from bytes #279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Try from bytes #279
Conversation
de67836 to
72ddaae
Compare
src/derive_util.rs
Outdated
| // are valid, which means that the struct itself is valid. | ||
| // That is the only precondition of `assume_valid_ref`. | ||
| let slf = unsafe { bytes.assume_valid_ref() }; | ||
| // TODO: What about interior mutability? One approach would |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, we just forbid interior mutability for TryFromBytes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the comment.
While we're on the subject, though, how do we actually ban interior mutability in MaybeValid? Currently, we implement AsMaybeUninit for all sized types and slices of sized types, which means that impls like the following exist automatically thanks to those blanket impls:
unsafe impl<T> AsMaybeUninit for UnsafeCell<T> {
type MaybeUninit = MaybeUninit<UnsafeCell<T>>;
}That, in turn, means that it's valid to write MaybeValid<UnsafeCell<T>>.
Maybe we could add safety preconditions to methods like assume_valid_ref that T::MaybeUninit doesn't contain any UnsafeCells? Then, so long as we only implement TryFromBytes for types without UnsafeCells, and we only ever emit calls to assume_valid_ref in such derived impls, then we should be okay.
Any other ideas? It'd be nice to not have this be so viral.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: See rust-lang/unsafe-code-guidelines#455 for more discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do we actually ban interior mutability in MaybeValid
There's no reason to. The only place where shared mutability gets involved in MaybeValid (through e.g. try_into_ref(&self) -> &T) is through the TryFromBytes::is_bit_valid method - that's the one that needs guarantees that there is only a single reference, either through unsafe or through banning UnsafeCell for TryFromBytes entirely.
That, in turn, means that it's valid to write MaybeValid<UnsafeCell>.
Yes, you can go from &mut MaybeValid<T> to &mut T on any type via a .write(). That's fine though - this doesn't involve shared mutability due to &mut.
Maybe we could add safety preconditions to methods like assume_valid_ref that T::MaybeUninit doesn't contain any UnsafeCells
Again, I don't see why this is in any way necessary.
See rust-lang/unsafe-code-guidelines#455 for more discussion.
That's essentially everything I told you offline - SB bans it, TB is fine. MaybeUninit does not interact with shared mutability, and neither should the AsMaybeUninit trait.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I believe I've completely handled the interior mutability issue with this update. Lmk if this looks sound to you?
For a bit more detail on my thinking (I think we're both on the same page now, but just to be sure), I had two concerns:
- Is it sound to have
&MaybeValid<T>and&Tat the same time? This is required in order to call custom validators.- This is now sound thanks to the requirement that
TandT::MaybeUninithaveUnsafeCells at the same offsets.
- This is now sound thanks to the requirement that
- Is it sound to have
&MaybeValid<T>and&[u8]at the same time? This is required in order to implementtry_from_refand friends since they take a&[u8]and convert it to a&MaybeValid<T>in order to callis_bit_valid.- This is sound so long as
T::MaybeUninitdoesn't contain anyUnsafeCells. Thanks to theUnsafeCells-at-the-same-locations requirement, this is sound so long asTdoesn't contain anyUnsafeCells. Since we only implementFromZeroesandFromBytesforMaybeValid<T>whereT: TryFromBytes(and sinceT: TryFromBytesbansUnsafeCells), we're okay. Given those impls, we only use safe code (namely,RefandFromBytesmethods) to convert&[u8]to&MaybeValid<T>.
- This is sound so long as
38d943c to
cc0a52d
Compare
| // SAFETY: TODO | ||
| // TODO: Why is this `allow` necessary? Why is it acceptable? | ||
| #[allow(clippy::as_conversions)] | ||
| let $candidate = unsafe { &*(candidate as *const MaybeValid<Self> as *const $repr) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kupiakos since you're the as-casting expert, would you mind taking a look at the as casts in this PR and confirming that they're kosher? I'm pretty sure they are, but Clippy is making me question my sanity...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not able to reproduce the clippy warning on the playground. Are you running a version of clippy with a bug?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only produced in 1.61 (our MSRV, which is tested in CI).
79826fb to
1ecc13a
Compare
| /// | ||
| /// When implementing `TryFromBytes`: | ||
| /// - If no `is_bit_valid` impl is provided, then it must be valid for | ||
| /// `is_bit_valid` to unconditionally return `true`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why allow there to be no is_bit_valid impl at all? Why would true implementations be common enough to justify a default method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used whenever you're implementing TryFromBytes for a type which is also FromBytes - in this PR, ~30 times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I would instead have that be covered by the implementing macro instead of exposing that as an extra thing for external users to consider
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could do that, it'd just make the usage sites a lot more verbose. Currently, you can do:
unsafe_impl!(u8: TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned);
unsafe_impl!(i8: TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned);
unsafe_impl!(u16: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i16: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u32: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i32: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u64: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i64: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u128: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i128: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(usize: TryFromBytes, FromZeroes, FromBytes, AsBytes);
unsafe_impl!(isize: TryFromBytes, FromZeroes, FromBytes, AsBytes);If you needed to supply a closure, you'd have to do:
unsafe_impl!(u8: FromZeroes, FromBytes, AsBytes, Unaligned);
unsafe_impl!(i8: FromZeroes, FromBytes, AsBytes, Unaligned);
unsafe_impl!(u16: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i16: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u32: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i32: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u64: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i64: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u128: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(i128: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(usize: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(isize: FromZeroes, FromBytes, AsBytes);
unsafe_impl!(u8: TryFromBytes; |_: &MaybeValid<u8>| true);
unsafe_impl!(i8: TryFromBytes; |_: &MaybeValid<i8>| true);
unsafe_impl!(u16: TryFromBytes; |_: &MaybeValid<u16>| true);
unsafe_impl!(i16: TryFromBytes; |_: &MaybeValid<i16>| true);
unsafe_impl!(u32: TryFromBytes; |_: &MaybeValid<u32>| true);
unsafe_impl!(i32: TryFromBytes; |_: &MaybeValid<i32>| true);
unsafe_impl!(u64: TryFromBytes; |_: &MaybeValid<u64>| true);
unsafe_impl!(i64: TryFromBytes; |_: &MaybeValid<i64>| true);
unsafe_impl!(u128: TryFromBytes; |_: &MaybeValid<u128>| true);
unsafe_impl!(i128: TryFromBytes; |_: &MaybeValid<i128>| true);
unsafe_impl!(usize: TryFromBytes; |_: &MaybeValid<usize>| true);
unsafe_impl!(isize: TryFromBytes; |_: &MaybeValid<isize>| true);Since we require safety comments on all impls, you're forced to write down a justification for why a type is TryFromBytes. IMO it'd be pretty hard for both you and the reviewer to fail to notice that you'd implemented TryFromBytes unconditionally on a type that isn't actually TryFromBytes. Even if you didn't know how the macro works (namely, that omitting the validator means all bit patterns are valid), you presumably would be unsure of the conditions under which the type was TryFromBytes and would go read the macro docs to see how it behaves.
2e68ab3 to
ac56c20
Compare
Makes progress on #196
ac56c20 to
0c35d00
Compare
TODO: - Finish writing safety comments - Finish writing documentation - Add tests Makes progress on #5
0c35d00 to
b042fa0
Compare
|
|
||
| impl<T> Default for MaybeValid<T> { | ||
| fn default() -> MaybeValid<T> { | ||
| MaybeValid { inner: MaybeUninit::uninit() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unsound! It violates the initialization invariant of MaybeValid. This should be MaybeUninit::zeroed instead.
Feel free to review or comment, but DO NOT MERGE; field projection needs to be landed in a separate PR first before this can be merged.