Skip to content

Commit 7d3a8f9

Browse files
authored
Fix soundness hole in Ref::into_ref and into_mut (#721)
This commit implements the fix for #716 which will be released as a new version in version trains 0.2, 0.3, 0.4, 0.5, 0.6, and 0.7. See #716 for a description of the soundness hole and an explanation of why this fix is chosen. Unfortunately, due to dtolnay/trybuild#241, there is no way for us to write a UI test that will detect a failure post-monomorphization, which is when the code implemented in this change is designed to fail. I have manually verified that unsound uses of these APIs now fail to compile. Release 0.7.31.
1 parent 961612f commit 7d3a8f9

File tree

4 files changed

+201
-30
lines changed

4 files changed

+201
-30
lines changed

Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
[package]
1616
edition = "2018"
1717
name = "zerocopy"
18-
version = "0.7.30"
18+
version = "0.7.31"
1919
authors = ["Joshua Liebow-Feeser <[email protected]>"]
2020
description = "Utilities for zero-copy parsing and serialization"
2121
license = "BSD-2-Clause OR Apache-2.0 OR MIT"
@@ -49,7 +49,7 @@ simd-nightly = ["simd"]
4949
__internal_use_only_features_that_work_on_stable = ["alloc", "derive", "simd"]
5050

5151
[dependencies]
52-
zerocopy-derive = { version = "=0.7.30", path = "zerocopy-derive", optional = true }
52+
zerocopy-derive = { version = "=0.7.31", path = "zerocopy-derive", optional = true }
5353

5454
[dependencies.byteorder]
5555
version = "1.3"
@@ -60,7 +60,7 @@ optional = true
6060
# zerocopy-derive remain equal, even if the 'derive' feature isn't used.
6161
# See: https://github.com/matklad/macro-dep-test
6262
[target.'cfg(any())'.dependencies]
63-
zerocopy-derive = { version = "=0.7.30", path = "zerocopy-derive" }
63+
zerocopy-derive = { version = "=0.7.31", path = "zerocopy-derive" }
6464

6565
[dev-dependencies]
6666
assert_matches = "1.5"
@@ -75,6 +75,6 @@ testutil = { path = "testutil" }
7575
# CI test failures.
7676
trybuild = { version = "=1.0.85", features = ["diff"] }
7777
# In tests, unlike in production, zerocopy-derive is not optional
78-
zerocopy-derive = { version = "=0.7.30", path = "zerocopy-derive" }
78+
zerocopy-derive = { version = "=0.7.31", path = "zerocopy-derive" }
7979
# TODO(#381) Remove this dependency once we have our own layout gadgets.
8080
elain = "0.3.0"

src/lib.rs

+78-25
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ mod macros;
249249
pub mod byteorder;
250250
#[doc(hidden)]
251251
pub mod macro_util;
252+
mod post_monomorphization_compile_fail_tests;
252253
mod util;
253254
// TODO(#252): If we make this pub, come up with a better name.
254255
mod wrappers;
@@ -4644,12 +4645,12 @@ where
46444645
/// `into_ref` consumes the `Ref`, and returns a reference to `T`.
46454646
#[inline(always)]
46464647
pub fn into_ref(self) -> &'a T {
4647-
// SAFETY: This is sound because `B` is guaranteed to live for the
4648-
// lifetime `'a`, meaning that a) the returned reference cannot outlive
4649-
// the `B` from which `self` was constructed and, b) no mutable methods
4650-
// on that `B` can be called during the lifetime of the returned
4651-
// reference. See the documentation on `deref_helper` for what
4652-
// invariants we are required to uphold.
4648+
assert!(B::INTO_REF_INTO_MUT_ARE_SOUND);
4649+
4650+
// SAFETY: According to the safety preconditions on
4651+
// `ByteSlice::INTO_REF_INTO_MUT_ARE_SOUND`, the preceding assert
4652+
// ensures that, given `B: 'a`, it is sound to drop `self` and still
4653+
// access the underlying memory using reads for `'a`.
46534654
unsafe { self.deref_helper() }
46544655
}
46554656
}
@@ -4664,12 +4665,13 @@ where
46644665
/// `into_mut` consumes the `Ref`, and returns a mutable reference to `T`.
46654666
#[inline(always)]
46664667
pub fn into_mut(mut self) -> &'a mut T {
4667-
// SAFETY: This is sound because `B` is guaranteed to live for the
4668-
// lifetime `'a`, meaning that a) the returned reference cannot outlive
4669-
// the `B` from which `self` was constructed and, b) no other methods -
4670-
// mutable or immutable - on that `B` can be called during the lifetime
4671-
// of the returned reference. See the documentation on
4672-
// `deref_mut_helper` for what invariants we are required to uphold.
4668+
assert!(B::INTO_REF_INTO_MUT_ARE_SOUND);
4669+
4670+
// SAFETY: According to the safety preconditions on
4671+
// `ByteSlice::INTO_REF_INTO_MUT_ARE_SOUND`, the preceding assert
4672+
// ensures that, given `B: 'a + ByteSliceMut`, it is sound to drop
4673+
// `self` and still access the underlying memory using both reads and
4674+
// writes for `'a`.
46734675
unsafe { self.deref_mut_helper() }
46744676
}
46754677
}
@@ -4684,12 +4686,12 @@ where
46844686
/// `into_slice` consumes the `Ref`, and returns a reference to `[T]`.
46854687
#[inline(always)]
46864688
pub fn into_slice(self) -> &'a [T] {
4687-
// SAFETY: This is sound because `B` is guaranteed to live for the
4688-
// lifetime `'a`, meaning that a) the returned reference cannot outlive
4689-
// the `B` from which `self` was constructed and, b) no mutable methods
4690-
// on that `B` can be called during the lifetime of the returned
4691-
// reference. See the documentation on `deref_slice_helper` for what
4692-
// invariants we are required to uphold.
4689+
assert!(B::INTO_REF_INTO_MUT_ARE_SOUND);
4690+
4691+
// SAFETY: According to the safety preconditions on
4692+
// `ByteSlice::INTO_REF_INTO_MUT_ARE_SOUND`, the preceding assert
4693+
// ensures that, given `B: 'a`, it is sound to drop `self` and still
4694+
// access the underlying memory using reads for `'a`.
46934695
unsafe { self.deref_slice_helper() }
46944696
}
46954697
}
@@ -4705,13 +4707,13 @@ where
47054707
/// `[T]`.
47064708
#[inline(always)]
47074709
pub fn into_mut_slice(mut self) -> &'a mut [T] {
4708-
// SAFETY: This is sound because `B` is guaranteed to live for the
4709-
// lifetime `'a`, meaning that a) the returned reference cannot outlive
4710-
// the `B` from which `self` was constructed and, b) no other methods -
4711-
// mutable or immutable - on that `B` can be called during the lifetime
4712-
// of the returned reference. See the documentation on
4713-
// `deref_mut_slice_helper` for what invariants we are required to
4714-
// uphold.
4710+
assert!(B::INTO_REF_INTO_MUT_ARE_SOUND);
4711+
4712+
// SAFETY: According to the safety preconditions on
4713+
// `ByteSlice::INTO_REF_INTO_MUT_ARE_SOUND`, the preceding assert
4714+
// ensures that, given `B: 'a + ByteSliceMut`, it is sound to drop
4715+
// `self` and still access the underlying memory using both reads and
4716+
// writes for `'a`.
47154717
unsafe { self.deref_mut_slice_helper() }
47164718
}
47174719
}
@@ -5127,6 +5129,29 @@ mod sealed {
51275129
pub unsafe trait ByteSlice:
51285130
Deref<Target = [u8]> + Sized + self::sealed::ByteSliceSealed
51295131
{
5132+
/// Are the [`Ref::into_ref`] and [`Ref::into_mut`] methods sound when used
5133+
/// with `Self`? If not, evaluating this constant must panic at compile
5134+
/// time.
5135+
///
5136+
/// This exists to work around #716 on versions of zerocopy prior to 0.8.
5137+
///
5138+
/// # Safety
5139+
///
5140+
/// This may only be set to true if the following holds: Given the
5141+
/// following:
5142+
/// - `Self: 'a`
5143+
/// - `bytes: Self`
5144+
/// - `let ptr = bytes.as_ptr()`
5145+
///
5146+
/// ...then:
5147+
/// - Using `ptr` to read the memory previously addressed by `bytes` is
5148+
/// sound for `'a` even after `bytes` has been dropped.
5149+
/// - If `Self: ByteSliceMut`, using `ptr` to write the memory previously
5150+
/// addressed by `bytes` is sound for `'a` even after `bytes` has been
5151+
/// dropped.
5152+
#[doc(hidden)]
5153+
const INTO_REF_INTO_MUT_ARE_SOUND: bool;
5154+
51305155
/// Gets a raw pointer to the first byte in the slice.
51315156
#[inline]
51325157
fn as_ptr(&self) -> *const u8 {
@@ -5161,6 +5186,10 @@ impl<'a> sealed::ByteSliceSealed for &'a [u8] {}
51615186
// TODO(#429): Add a "SAFETY" comment and remove this `allow`.
51625187
#[allow(clippy::undocumented_unsafe_blocks)]
51635188
unsafe impl<'a> ByteSlice for &'a [u8] {
5189+
// SAFETY: If `&'b [u8]: 'a`, then the underlying memory is treated as
5190+
// borrowed immutably for `'a` even if the slice itself is dropped.
5191+
const INTO_REF_INTO_MUT_ARE_SOUND: bool = true;
5192+
51645193
#[inline]
51655194
fn split_at(self, mid: usize) -> (Self, Self) {
51665195
<[u8]>::split_at(self, mid)
@@ -5171,6 +5200,10 @@ impl<'a> sealed::ByteSliceSealed for &'a mut [u8] {}
51715200
// TODO(#429): Add a "SAFETY" comment and remove this `allow`.
51725201
#[allow(clippy::undocumented_unsafe_blocks)]
51735202
unsafe impl<'a> ByteSlice for &'a mut [u8] {
5203+
// SAFETY: If `&'b mut [u8]: 'a`, then the underlying memory is treated as
5204+
// borrowed mutably for `'a` even if the slice itself is dropped.
5205+
const INTO_REF_INTO_MUT_ARE_SOUND: bool = true;
5206+
51745207
#[inline]
51755208
fn split_at(self, mid: usize) -> (Self, Self) {
51765209
<[u8]>::split_at_mut(self, mid)
@@ -5181,6 +5214,16 @@ impl<'a> sealed::ByteSliceSealed for cell::Ref<'a, [u8]> {}
51815214
// TODO(#429): Add a "SAFETY" comment and remove this `allow`.
51825215
#[allow(clippy::undocumented_unsafe_blocks)]
51835216
unsafe impl<'a> ByteSlice for cell::Ref<'a, [u8]> {
5217+
const INTO_REF_INTO_MUT_ARE_SOUND: bool = if !cfg!(doc) {
5218+
panic!("Ref::into_ref and Ref::into_mut are unsound when used with core::cell::Ref; see https://github.com/google/zerocopy/issues/716")
5219+
} else {
5220+
// When compiling documentation, allow the evaluation of this constant
5221+
// to succeed. This doesn't represent a soundness hole - it just delays
5222+
// any error to runtime. The reason we need this is that, otherwise,
5223+
// `rustdoc` will fail when trying to document this item.
5224+
false
5225+
};
5226+
51845227
#[inline]
51855228
fn split_at(self, mid: usize) -> (Self, Self) {
51865229
cell::Ref::map_split(self, |slice| <[u8]>::split_at(slice, mid))
@@ -5191,6 +5234,16 @@ impl<'a> sealed::ByteSliceSealed for RefMut<'a, [u8]> {}
51915234
// TODO(#429): Add a "SAFETY" comment and remove this `allow`.
51925235
#[allow(clippy::undocumented_unsafe_blocks)]
51935236
unsafe impl<'a> ByteSlice for RefMut<'a, [u8]> {
5237+
const INTO_REF_INTO_MUT_ARE_SOUND: bool = if !cfg!(doc) {
5238+
panic!("Ref::into_ref and Ref::into_mut are unsound when used with core::cell::RefMut; see https://github.com/google/zerocopy/issues/716")
5239+
} else {
5240+
// When compiling documentation, allow the evaluation of this constant
5241+
// to succeed. This doesn't represent a soundness hole - it just delays
5242+
// any error to runtime. The reason we need this is that, otherwise,
5243+
// `rustdoc` will fail when trying to document this item.
5244+
false
5245+
};
5246+
51945247
#[inline]
51955248
fn split_at(self, mid: usize) -> (Self, Self) {
51965249
RefMut::map_split(self, |slice| <[u8]>::split_at_mut(slice, mid))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2018 The Fuchsia Authors
2+
//
3+
// Licensed under the 2-Clause BSD License <LICENSE-BSD or
4+
// https://opensource.org/license/bsd-2-clause>, Apache License, Version 2.0
5+
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
6+
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
7+
// This file may not be copied, modified, or distributed except according to
8+
// those terms.
9+
10+
//! Code that should fail to compile during the post-monomorphization compiler
11+
//! pass.
12+
//!
13+
//! Due to [a limitation with the `trybuild` crate][trybuild-issue], we cannot
14+
//! use our UI testing framework to test compilation failures that are
15+
//! encountered after monomorphization has complated. This module has one item
16+
//! for each such test we would prefer to have as a UI test, with the code in
17+
//! question appearing as a rustdoc example which is marked with `compile_fail`.
18+
//! This has the effect of causing doctests to fail if any of these examples
19+
//! compile successfully.
20+
//!
21+
//! This is very much a hack and not a complete replacement for UI tests - most
22+
//! notably because this only provides a single "compile vs fail" bit of
23+
//! information, but does not allow us to depend upon the specific error that
24+
//! causes compilation to fail.
25+
//!
26+
//! [trybuild-issue]: https://github.com/dtolnay/trybuild/issues/241
27+
28+
// Miri doesn't detect post-monimorphization failures as compile-time failures,
29+
// but instead as runtime failures.
30+
#![cfg(not(miri))]
31+
32+
/// ```compile_fail
33+
/// use core::cell::{Ref, RefCell};
34+
///
35+
/// let refcell = RefCell::new([0u8, 1, 2, 3]);
36+
/// let core_ref = refcell.borrow();
37+
/// let core_ref = Ref::map(core_ref, |bytes| &bytes[..]);
38+
///
39+
/// // `zc_ref` now stores `core_ref` internally.
40+
/// let zc_ref = zerocopy::Ref::<_, u32>::new(core_ref).unwrap();
41+
///
42+
/// // This causes `core_ref` to get dropped and synthesizes a Rust
43+
/// // reference to the memory `core_ref` was pointing at.
44+
/// let rust_ref = zc_ref.into_ref();
45+
///
46+
/// // UB!!! This mutates `rust_ref`'s referent while it's alive.
47+
/// *refcell.borrow_mut() = [0, 0, 0, 0];
48+
///
49+
/// println!("{}", rust_ref);
50+
/// ```
51+
#[allow(unused)]
52+
const REFCELL_REF_INTO_REF: () = ();
53+
54+
/// ```compile_fail
55+
/// use core::cell::{RefCell, RefMut};
56+
///
57+
/// let refcell = RefCell::new([0u8, 1, 2, 3]);
58+
/// let core_ref_mut = refcell.borrow_mut();
59+
/// let core_ref_mut = RefMut::map(core_ref_mut, |bytes| &mut bytes[..]);
60+
///
61+
/// // `zc_ref` now stores `core_ref_mut` internally.
62+
/// let zc_ref = zerocopy::Ref::<_, u32>::new(core_ref_mut).unwrap();
63+
///
64+
/// // This causes `core_ref_mut` to get dropped and synthesizes a Rust
65+
/// // reference to the memory `core_ref` was pointing at.
66+
/// let rust_ref_mut = zc_ref.into_mut();
67+
///
68+
/// // UB!!! This mutates `rust_ref_mut`'s referent while it's alive.
69+
/// *refcell.borrow_mut() = [0, 0, 0, 0];
70+
///
71+
/// println!("{}", rust_ref_mut);
72+
/// ```
73+
#[allow(unused)]
74+
const REFCELL_REFMUT_INTO_MUT: () = ();
75+
76+
/// ```compile_fail
77+
/// use core::cell::{Ref, RefCell};
78+
///
79+
/// let refcell = RefCell::new([0u8, 1, 2, 3]);
80+
/// let core_ref = refcell.borrow();
81+
/// let core_ref = Ref::map(core_ref, |bytes| &bytes[..]);
82+
///
83+
/// // `zc_ref` now stores `core_ref` internally.
84+
/// let zc_ref = zerocopy::Ref::<_, [u16]>::new_slice(core_ref).unwrap();
85+
///
86+
/// // This causes `core_ref` to get dropped and synthesizes a Rust
87+
/// // reference to the memory `core_ref` was pointing at.
88+
/// let rust_ref = zc_ref.into_slice();
89+
///
90+
/// // UB!!! This mutates `rust_ref`'s referent while it's alive.
91+
/// *refcell.borrow_mut() = [0, 0, 0, 0];
92+
///
93+
/// println!("{:?}", rust_ref);
94+
/// ```
95+
#[allow(unused)]
96+
const REFCELL_REFMUT_INTO_SLICE: () = ();
97+
98+
/// ```compile_fail
99+
/// use core::cell::{RefCell, RefMut};
100+
///
101+
/// let refcell = RefCell::new([0u8, 1, 2, 3]);
102+
/// let core_ref_mut = refcell.borrow_mut();
103+
/// let core_ref_mut = RefMut::map(core_ref_mut, |bytes| &mut bytes[..]);
104+
///
105+
/// // `zc_ref` now stores `core_ref_mut` internally.
106+
/// let zc_ref = zerocopy::Ref::<_, [u16]>::new_slice(core_ref_mut).unwrap();
107+
///
108+
/// // This causes `core_ref_mut` to get dropped and synthesizes a Rust
109+
/// // reference to the memory `core_ref` was pointing at.
110+
/// let rust_ref_mut = zc_ref.into_mut_slice();
111+
///
112+
/// // UB!!! This mutates `rust_ref_mut`'s referent while it's alive.
113+
/// *refcell.borrow_mut() = [0, 0, 0, 0];
114+
///
115+
/// println!("{:?}", rust_ref_mut);
116+
/// ```
117+
#[allow(unused)]
118+
const REFCELL_REFMUT_INTO_MUT_SLICE: () = ();

zerocopy-derive/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[package]
1010
edition = "2018"
1111
name = "zerocopy-derive"
12-
version = "0.7.30"
12+
version = "0.7.31"
1313
authors = ["Joshua Liebow-Feeser <[email protected]>"]
1414
description = "Custom derive for traits from the zerocopy crate"
1515
license = "BSD-2-Clause OR Apache-2.0 OR MIT"

0 commit comments

Comments
 (0)