Skip to content

Commit 367e68b

Browse files
joshlfjswrenn
andauthored
[pointer] Improve soundness of invariant modeling (#2397)
This commit makes the following improvements: - Removes the `Inaccessible` aliasing mode. This mode was not unsound, but it was unnecessary in practice. - Replaces `Unknown` with `Unaligned` for alignment. - Replaces `Unknown` with `Uninit` for validity. Finally, with the exception of `transparent_wrapper_into_inner`, this commit ensures that all `Ptr` methods which modify the type or validity of a `Ptr` are sound. Previously, we modeled validity as "knowledge" about the `Ptr`'s referent (see #1866 for a more in-depth explanation). In particular, we assumed that it was always sound to "forget" information about a `Ptr`'s referent in the same way in which it is sound to "forget" that a `Ptr` is validly-aligned, converting a `Ptr<T, (_, Aligned, _)>` to a `Ptr<T, (_, Unaligned, _)>`. The problem with this approach is that validity doesn't just specify what bit patterns can be expected to be read from a `Ptr`'s referent, it also specifies what bit patterns are permitted to be *written* to the referent. Thus, "forgetting" about validity and expanding the set of expected bit patterns also expands the set of bit patterns which can be written. Consider, for example, "forgetting" that a `Ptr<bool, (_, _, Valid)>` is bit-valid, and downgrading it to a `Ptr<bool, (_, _, Initialized)>`. If the aliasing is `Exclusive`, then the resulting `Ptr` would permit writing arbitrary `u8`s to the referent, violating the bit validity of the `bool`. This commit moves us in the direction of a new model, in which changes to the referent type or the validity of a `Ptr` must abide by the following rules: - If the resulting `Ptr` permits mutation of its referent (either via interior mutation or `Exclusive` aliasing), then the set of allowed bit patterns in the referent may not expand - If the original `Ptr` permits mutation of its referent while the resulting `Ptr` is also live (i.e., via interior mutation on a `Shared` `Ptr`), then the set of allowed bit patterns in the referent may not shrink This commit does not fix `transparent_wrapper_into_inner`, which will require an overhaul or replacement of the `TransparentWrapper` trait. Makes progress on #2226, #1866 gherrit-pr-id: I95d6c5cd23eb5ea6629cd6e4b99696913b1ded93 Co-authored-by: Jack Wrenn <jswrenn@amazon.com>
1 parent 4173443 commit 367e68b

21 files changed

+360
-216
lines changed

src/pointer/invariant.rs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,6 @@ pub trait Validity: Sealed {}
5252
/// Exclusive`.
5353
pub trait Reference: Aliasing + Sealed {}
5454

55-
/// It is unknown whether any invariant holds.
56-
pub enum Unknown {}
57-
58-
impl Alignment for Unknown {}
59-
impl Validity for Unknown {}
60-
61-
/// The `Ptr<'a, T>` does not permit any reads or writes from or to its referent.
62-
pub enum Inaccessible {}
63-
64-
impl Aliasing for Inaccessible {
65-
const IS_EXCLUSIVE: bool = false;
66-
}
67-
6855
/// The `Ptr<'a, T>` adheres to the aliasing rules of a `&'a T`.
6956
///
7057
/// The referent of a shared-aliased `Ptr` may be concurrently referenced by any
@@ -90,11 +77,21 @@ impl Aliasing for Exclusive {
9077
}
9178
impl Reference for Exclusive {}
9279

80+
/// It is unknown whether the pointer is aligned.
81+
pub enum Unaligned {}
82+
83+
impl Alignment for Unaligned {}
84+
9385
/// The referent is aligned: for `Ptr<T>`, the referent's address is a multiple
9486
/// of the `T`'s alignment.
9587
pub enum Aligned {}
9688
impl Alignment for Aligned {}
9789

90+
/// Any bit pattern is allowed in the `Ptr`'s referent, including uninitialized
91+
/// bytes.
92+
pub enum Uninit {}
93+
impl Validity for Uninit {}
94+
9895
/// The byte ranges initialized in `T` are also initialized in the referent.
9996
///
10097
/// Formally: uninitialized bytes may only be present in `Ptr<T>`'s referent
@@ -133,6 +130,17 @@ impl Validity for Initialized {}
133130
pub enum Valid {}
134131
impl Validity for Valid {}
135132

133+
/// # Safety
134+
///
135+
/// `DT: CastableFrom<ST, SV, DV>` is sound if `SV = DV = Uninit` or `SV = DV =
136+
/// Initialized`.
137+
pub unsafe trait CastableFrom<ST: ?Sized, SV, DV> {}
138+
139+
// SAFETY: `SV = DV = Uninit`.
140+
unsafe impl<ST: ?Sized, DT: ?Sized> CastableFrom<ST, Uninit, Uninit> for DT {}
141+
// SAFETY: `SV = DV = Initialized`.
142+
unsafe impl<ST: ?Sized, DT: ?Sized> CastableFrom<ST, Initialized, Initialized> for DT {}
143+
136144
/// [`Ptr`](crate::Ptr) referents that permit unsynchronized read operations.
137145
///
138146
/// `T: Read<A, R>` implies that a pointer to `T` with aliasing `A` permits
@@ -175,14 +183,13 @@ mod sealed {
175183

176184
pub trait Sealed {}
177185

178-
impl Sealed for Unknown {}
179-
180-
impl Sealed for Inaccessible {}
181186
impl Sealed for Shared {}
182187
impl Sealed for Exclusive {}
183188

189+
impl Sealed for Unaligned {}
184190
impl Sealed for Aligned {}
185191

192+
impl Sealed for Uninit {}
186193
impl Sealed for AsInitialized {}
187194
impl Sealed for Initialized {}
188195
impl Sealed for Valid {}

src/pointer/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ use crate::Unaligned;
2424
/// to [`TryFromBytes::is_bit_valid`].
2525
///
2626
/// [`TryFromBytes::is_bit_valid`]: crate::TryFromBytes::is_bit_valid
27-
pub type Maybe<'a, T, Aliasing = invariant::Shared, Alignment = invariant::Unknown> =
27+
pub type Maybe<'a, T, Aliasing = invariant::Shared, Alignment = invariant::Unaligned> =
2828
Ptr<'a, T, (Aliasing, Alignment, invariant::Initialized)>;
2929

3030
/// A semi-user-facing wrapper type representing a maybe-aligned reference, for
3131
/// use in [`TryFromBytes::is_bit_valid`].
3232
///
3333
/// [`TryFromBytes::is_bit_valid`]: crate::TryFromBytes::is_bit_valid
34-
pub type MaybeAligned<'a, T, Aliasing = invariant::Shared, Alignment = invariant::Unknown> =
34+
pub type MaybeAligned<'a, T, Aliasing = invariant::Shared, Alignment = invariant::Unaligned> =
3535
Ptr<'a, T, (Aliasing, Alignment, invariant::Valid)>;
3636

3737
// These methods are defined on the type alias, `MaybeAligned`, so as to bring

src/pointer/ptr.rs

Lines changed: 201 additions & 108 deletions
Large diffs are not rendered by default.

src/util/macro_util.rs

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use core::ptr::{self, NonNull};
2626

2727
use crate::{
2828
pointer::invariant::{self, BecauseExclusive, BecauseImmutable, Invariants, ReadReason},
29-
FromBytes, Immutable, IntoBytes, Ptr, TryFromBytes, Unalign, ValidityError,
29+
FromBytes, Immutable, IntoBytes, Ptr, TryFromBytes, ValidityError,
3030
};
3131

3232
/// Projects the type of the field at `Index` in `Self`.
@@ -529,10 +529,15 @@ pub unsafe fn transmute_mut<'dst, 'src: 'dst, Src: 'src, Dst: 'dst>(
529529
///
530530
/// # Safety
531531
///
532-
/// Unsafe code may assume that, if `try_cast_or_pme(src)` returns `Some`,
532+
/// Unsafe code may assume that, if `try_cast_or_pme(src)` returns `Ok`,
533533
/// `*src` is a bit-valid instance of `Dst`, and that the size of `Src` is
534534
/// greater than or equal to the size of `Dst`.
535535
///
536+
/// Unsafe code may assume that, if `try_cast_or_pme(src)` returns `Err`, the
537+
/// encapsulated `Ptr` value is the original `src`. `try_cast_or_pme` cannot
538+
/// guarantee that the referent has not been modified, as it calls user-defined
539+
/// code (`TryFromBytes::is_bit_valid`).
540+
///
536541
/// # Panics
537542
///
538543
/// `try_cast_or_pme` may either produce a post-monomorphization error or a
@@ -545,15 +550,15 @@ pub unsafe fn transmute_mut<'dst, 'src: 'dst, Src: 'src, Dst: 'dst>(
545550
fn try_cast_or_pme<Src, Dst, I, R>(
546551
src: Ptr<'_, Src, I>,
547552
) -> Result<
548-
Ptr<'_, Dst, (I::Aliasing, invariant::Unknown, invariant::Valid)>,
553+
Ptr<'_, Dst, (I::Aliasing, invariant::Unaligned, invariant::Valid)>,
549554
ValidityError<Ptr<'_, Src, I>, Dst>,
550555
>
551556
where
552557
// TODO(#2226): There should be a `Src: FromBytes` bound here, but doing so
553558
// requires deeper surgery.
554-
Src: IntoBytes + invariant::Read<I::Aliasing, R>,
559+
Src: invariant::Read<I::Aliasing, R>,
555560
Dst: TryFromBytes + invariant::Read<I::Aliasing, R>,
556-
I: Invariants<Validity = invariant::Valid>,
561+
I: Invariants<Validity = invariant::Initialized>,
557562
I::Aliasing: invariant::Reference,
558563
R: ReadReason,
559564
{
@@ -567,11 +572,6 @@ where
567572
#[allow(clippy::as_conversions)]
568573
let c_ptr = unsafe { src.cast_unsized(|p| p as *mut Dst) };
569574

570-
// SAFETY: `c_ptr` is derived from `src` which is `IntoBytes`. By
571-
// invariant on `IntoByte`s, `c_ptr`'s referent consists entirely of
572-
// initialized bytes.
573-
let c_ptr = unsafe { c_ptr.assume_initialized() };
574-
575575
match c_ptr.try_into_valid() {
576576
Ok(ptr) => Ok(ptr),
577577
Err(err) => {
@@ -611,19 +611,44 @@ where
611611
Src: IntoBytes,
612612
Dst: TryFromBytes,
613613
{
614-
let mut src = ManuallyDrop::new(src);
615-
let ptr = Ptr::from_mut(&mut src);
616-
// Wrapping `Dst` in `Unalign` ensures that this cast does not fail due to
617-
// alignment requirements.
618-
match try_cast_or_pme::<_, ManuallyDrop<Unalign<Dst>>, _, BecauseExclusive>(ptr) {
619-
Ok(ptr) => {
620-
let dst = ptr.bikeshed_recall_aligned().as_mut();
621-
// SAFETY: By shadowing `dst`, we ensure that `dst` is not re-used
622-
// after taking its inner value.
623-
let dst = unsafe { ManuallyDrop::take(dst) };
624-
Ok(dst.into_inner())
625-
}
626-
Err(_) => Err(ValidityError::new(ManuallyDrop::into_inner(src))),
614+
static_assert!(Src, Dst => mem::size_of::<Dst>() == mem::size_of::<Src>());
615+
616+
let mu_src = mem::MaybeUninit::new(src);
617+
// SAFETY: By invariant on `&`, the following are satisfied:
618+
// - `&mu_src` is valid for reads
619+
// - `&mu_src` is properly aligned
620+
// - `&mu_src`'s referent is bit-valid
621+
let mu_src_copy = unsafe { core::ptr::read(&mu_src) };
622+
// SAFETY: `MaybeUninit` has no validity constraints.
623+
let mut mu_dst: mem::MaybeUninit<Dst> =
624+
unsafe { crate::util::transmute_unchecked(mu_src_copy) };
625+
626+
let ptr = Ptr::from_mut(&mut mu_dst);
627+
628+
// SAFETY: Since `Src: IntoBytes`, and since `size_of::<Src>() ==
629+
// size_of::<Dst>()` by the preceding assertion, all of `mu_dst`'s bytes are
630+
// initialized.
631+
let ptr = unsafe { ptr.assume_validity::<invariant::Initialized>() };
632+
633+
// SAFETY: `MaybeUninit<T>` and `T` have the same size [1], so this cast
634+
// preserves the referent's size. This cast preserves provenance.
635+
//
636+
// [1] Per https://doc.rust-lang.org/1.81.0/std/mem/union.MaybeUninit.html#layout-1:
637+
//
638+
// `MaybeUninit<T>` is guaranteed to have the same size, alignment, and
639+
// ABI as `T`
640+
let ptr: Ptr<'_, Dst, _> =
641+
unsafe { ptr.cast_unsized(|mu: *mut mem::MaybeUninit<Dst>| mu.cast()) };
642+
643+
if Dst::is_bit_valid(ptr.forget_aligned()) {
644+
// SAFETY: Since `Dst::is_bit_valid`, we know that `ptr`'s referent is
645+
// bit-valid for `Dst`. `ptr` points to `mu_dst`, and no intervening
646+
// operations have mutated it, so it is a bit-valid `Dst`.
647+
Ok(unsafe { mu_dst.assume_init() })
648+
} else {
649+
// SAFETY: `mu_src` was constructed from `src` and never modified, so it
650+
// is still bit-valid.
651+
Err(ValidityError::new(unsafe { mu_src.assume_init() }))
627652
}
628653
}
629654

@@ -645,15 +670,29 @@ where
645670
Src: IntoBytes + Immutable,
646671
Dst: TryFromBytes + Immutable,
647672
{
648-
match try_cast_or_pme::<Src, Dst, _, BecauseImmutable>(Ptr::from_ref(src)) {
673+
let ptr = Ptr::from_ref(src);
674+
let ptr = ptr.bikeshed_recall_initialized_immutable();
675+
match try_cast_or_pme::<Src, Dst, _, BecauseImmutable>(ptr) {
649676
Ok(ptr) => {
650677
static_assert!(Src, Dst => mem::align_of::<Dst>() <= mem::align_of::<Src>());
651678
// SAFETY: We have checked that `Dst` does not have a stricter
652679
// alignment requirement than `Src`.
653680
let ptr = unsafe { ptr.assume_alignment::<invariant::Aligned>() };
654681
Ok(ptr.as_ref())
655682
}
656-
Err(err) => Err(err.map_src(Ptr::as_ref)),
683+
Err(err) => Err(err.map_src(|ptr| {
684+
// SAFETY: Because `Src: Immutable` and we create a `Ptr` via
685+
// `Ptr::from_ref`, the resulting `Ptr` is a shared-and-`Immutable`
686+
// `Ptr`, which does not permit mutation of its referent. Therefore,
687+
// no mutation could have happened during the call to
688+
// `try_cast_or_pme` (any such mutation would be unsound).
689+
//
690+
// `try_cast_or_pme` promises to return its original argument, and
691+
// so we know that we are getting back the same `ptr` that we
692+
// originally passed, and that `ptr` was a bit-valid `Src`.
693+
let ptr = unsafe { ptr.assume_valid() };
694+
ptr.as_ref()
695+
})),
657696
}
658697
}
659698

@@ -675,15 +714,17 @@ where
675714
Src: FromBytes + IntoBytes,
676715
Dst: TryFromBytes + IntoBytes,
677716
{
678-
match try_cast_or_pme::<Src, Dst, _, BecauseExclusive>(Ptr::from_mut(src)) {
717+
let ptr = Ptr::from_mut(src);
718+
let ptr = ptr.bikeshed_recall_initialized_from_bytes();
719+
match try_cast_or_pme::<Src, Dst, _, BecauseExclusive>(ptr) {
679720
Ok(ptr) => {
680721
static_assert!(Src, Dst => mem::align_of::<Dst>() <= mem::align_of::<Src>());
681722
// SAFETY: We have checked that `Dst` does not have a stricter
682723
// alignment requirement than `Src`.
683724
let ptr = unsafe { ptr.assume_alignment::<invariant::Aligned>() };
684725
Ok(ptr.as_mut())
685726
}
686-
Err(err) => Err(err.map_src(Ptr::as_mut)),
727+
Err(err) => Err(err.map_src(|ptr| ptr.bikeshed_recall_valid().as_mut())),
687728
}
688729
}
689730

src/util/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ impl<I: invariant::Validity> ValidityVariance<I> for Covariant {
9999
pub enum Invariant {}
100100

101101
impl<I: invariant::Alignment> AlignmentVariance<I> for Invariant {
102-
type Applied = invariant::Unknown;
102+
type Applied = invariant::Unaligned;
103103
}
104104

105105
impl<I: invariant::Validity> ValidityVariance<I> for Invariant {
106-
type Applied = invariant::Unknown;
106+
type Applied = invariant::Uninit;
107107
}
108108

109109
// SAFETY:

tests/ui-msrv/diagnostic-not-implemented-unaligned.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
error[E0277]: the trait bound `NotZerocopy: Unaligned` is not satisfied
1+
error[E0277]: the trait bound `NotZerocopy: zerocopy::Unaligned` is not satisfied
22
--> tests/ui-msrv/diagnostic-not-implemented-unaligned.rs:18:23
33
|
44
18 | takes_unaligned::<NotZerocopy>();
5-
| ^^^^^^^^^^^ the trait `Unaligned` is not implemented for `NotZerocopy`
5+
| ^^^^^^^^^^^ the trait `zerocopy::Unaligned` is not implemented for `NotZerocopy`
66
|
77
note: required by a bound in `takes_unaligned`
88
--> tests/ui-msrv/diagnostic-not-implemented-unaligned.rs:21:23

tests/ui-nightly/diagnostic-not-implemented-unaligned.stderr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
error[E0277]: the trait bound `NotZerocopy: Unaligned` is not satisfied
1+
error[E0277]: the trait bound `NotZerocopy: zerocopy::Unaligned` is not satisfied
22
--> tests/ui-nightly/diagnostic-not-implemented-unaligned.rs:18:23
33
|
44
18 | takes_unaligned::<NotZerocopy>();
5-
| ^^^^^^^^^^^ the trait `Unaligned` is not implemented for `NotZerocopy`
5+
| ^^^^^^^^^^^ the trait `zerocopy::Unaligned` is not implemented for `NotZerocopy`
66
|
77
= note: Consider adding `#[derive(Unaligned)]` to `NotZerocopy`
8-
= help: the following other types implement trait `Unaligned`:
8+
= help: the following other types implement trait `zerocopy::Unaligned`:
99
()
1010
AtomicBool
1111
AtomicI8

tests/ui-stable/diagnostic-not-implemented-unaligned.stderr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
error[E0277]: the trait bound `NotZerocopy: Unaligned` is not satisfied
1+
error[E0277]: the trait bound `NotZerocopy: zerocopy::Unaligned` is not satisfied
22
--> tests/ui-stable/diagnostic-not-implemented-unaligned.rs:18:23
33
|
44
18 | takes_unaligned::<NotZerocopy>();
5-
| ^^^^^^^^^^^ the trait `Unaligned` is not implemented for `NotZerocopy`
5+
| ^^^^^^^^^^^ the trait `zerocopy::Unaligned` is not implemented for `NotZerocopy`
66
|
77
= note: Consider adding `#[derive(Unaligned)]` to `NotZerocopy`
8-
= help: the following other types implement trait `Unaligned`:
8+
= help: the following other types implement trait `zerocopy::Unaligned`:
99
()
1010
AtomicBool
1111
AtomicI8

zerocopy-derive/tests/include.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,12 @@ pub mod util {
118118

119119
let buf = super::imp::MaybeUninit::<T>::uninit();
120120
let ptr = super::imp::Ptr::from_ref(&buf);
121+
// SAFETY: This is intentionally unsound; see the preceding comment.
122+
let ptr = unsafe { ptr.assume_initialized() };
123+
121124
// SAFETY: `T` and `MaybeUninit<T>` have the same layout, so this is a
122125
// size-preserving cast. It is also a provenance-preserving cast.
123126
let ptr = unsafe { ptr.cast_unsized_unchecked(|p| p as *mut T) };
124-
// SAFETY: This is intentionally unsound; see the preceding comment.
125-
let ptr = unsafe { ptr.assume_initialized() };
126127
assert!(<T as super::imp::TryFromBytes>::is_bit_valid(ptr));
127128
}
128129
}

zerocopy-derive/tests/struct_try_from_bytes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ fn test_maybe_from_bytes() {
157157
// that we *don't* spuriously do that when generic parameters are present.
158158

159159
let candidate = ::zerocopy::Ptr::from_ref(&[2u8][..]);
160+
let candidate = candidate.bikeshed_recall_initialized_from_bytes();
160161

161162
// SAFETY:
162163
// - The cast preserves address and size. As a result, the cast will address

0 commit comments

Comments
 (0)