diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9cfc928c1..81a96db145 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -507,6 +507,73 @@ jobs: # `roll-pinned-toolchain-versions.yml`. kani-version: 0.55.0 + unsafe_fields: + runs-on: ubuntu-latest + needs: generate_cache + strategy: + # By default, this is set to `true`, which means that a single CI job + # failure will cause all outstanding jobs to be canceled. This slows down + # development because it means that errors need to be encountered and + # fixed one at a time. + fail-fast: false + matrix: + toolchain: [ + "msrv", + "stable", + "nightly", + ] + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: Configure environment variables + run: | + set -eo pipefail + ZC_TOOLCHAIN="$(./cargo.sh --version nightly)" + RUSTFLAGS="$RUSTFLAGS $ZC_NIGHTLY_RUSTFLAGS" + echo "RUSTFLAGS=$RUSTFLAGS" >> $GITHUB_ENV + echo "ZC_TOOLCHAIN=$ZC_TOOLCHAIN" >> $GITHUB_ENV + - name: Install stable Rust for use in 'cargo.sh' + uses: dtolnay/rust-toolchain@00b49be78f40fba4e87296b2ead62868750bdd83 # stable + with: + toolchain: stable + - name: Install Rust with nightly toolchain (${{ env.ZC_TOOLCHAIN }}) and target aarch64_be-unknown-linux-gnu + uses: dtolnay/rust-toolchain@00b49be78f40fba4e87296b2ead62868750bdd83 # stable + with: + toolchain: ${{ env.ZC_TOOLCHAIN }} + components: clippy, rust-src + - name: Check + run: ./cargo.sh +${{ matrix.toolchain }} check --package unsafe-fields --verbose + - name: Check tests + run: ./cargo.sh +${{ matrix.toolchain }} check --tests --package unsafe-fields --verbose + - name: Build + run: ./cargo.sh +${{ matrix.toolchain }} build --package unsafe-fields --verbose + - name: Run tests + run: ./cargo.sh +${{ matrix.toolchain }} test --package unsafe-fields --verbose + - name: Clippy + run: ./cargo.sh +${{ matrix.toolchain }} clippy --package unsafe-fields --verbose + - name: Clippy tests + run: ./cargo.sh +${{ matrix.toolchain }} clippy --package unsafe-fields --tests --verbose + # Clippy improves the accuracy of lints over time, and fixes bugs. Only + # running Clippy on nightly allows us to avoid having to write code + # which is compatible with older versions of Clippy, which sometimes + # requires hacks to work around limitations that are fixed in more + # recent versions. + if: matrix.toolchain == 'nightly' + - name: Cargo doc + # We pass --document-private-items and --document-hidden items to ensure + # that documentation always builds even for these items. This makes + # future changes to make those items public/non-hidden more painless. + # Note that --document-hidden-items is unstable; if a future release + # breaks or removes it, we can just update CI to no longer pass that + # flag. + run: | + # Include arguments passed during docs.rs deployments to make sure those + # work properly. + set -eo pipefail + METADATA_DOCS_RS_RUSTDOC_ARGS="$(cargo metadata --format-version 1 | \ + jq -r ".packages[] | select(.name == \"unsafe-fields\").metadata.docs.rs.\"rustdoc-args\"[]" | tr '\n' ' ')" + export RUSTDOCFLAGS="${{ matrix.toolchain == 'nightly' && '-Z unstable-options --document-hidden-items $METADATA_DOCS_RS_RUSTDOC_ARGS'|| '' }} $RUSTDOCFLAGS" + ./cargo.sh +${{ matrix.toolchain }} doc --document-private-items --package unsafe-fields + # NEON intrinsics are currently broken on big-endian platforms. [1] This test ensures # that we don't accidentally attempt to compile these intrinsics on such platforms. We # can't use this as part of the build matrix because rustup doesn't support the @@ -670,7 +737,7 @@ jobs: # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks if: failure() runs-on: ubuntu-latest - needs: [build_test, kani,check_be_aarch64 , check_fmt, check_readme, check_versions, generate_cache, check-all-toolchains-tested, check-job-dependencies, run-git-hooks] + needs: [build_test, kani,check_be_aarch64, check_fmt, check_readme, check_versions, generate_cache, check-all-toolchains-tested, check-job-dependencies, run-git-hooks, unsafe_fields] steps: - name: Mark the job as failed run: exit 1 diff --git a/Cargo.toml b/Cargo.toml index 4f2eb1aecd..355dc3c049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ testutil = { path = "testutil" } # sometimes change the output format slightly, so a version mismatch can cause # CI test failures. trybuild = { version = "=1.0.90", features = ["diff"] } +unsafe-fields = { path = "./unsafe-fields" } # In tests, unlike in production, zerocopy-derive is not optional zerocopy-derive = { version = "=0.9.0-alpha.0", path = "zerocopy-derive" } # TODO(#381) Remove this dependency once we have our own layout gadgets. diff --git a/unsafe-fields/Cargo.toml b/unsafe-fields/Cargo.toml new file mode 100644 index 0000000000..35a3a35356 --- /dev/null +++ b/unsafe-fields/Cargo.toml @@ -0,0 +1,21 @@ +# Copyright 2024 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +[package] +name = "unsafe-fields" +version = "0.1.0" +edition = "2021" +license = "BSD-2-Clause OR Apache-2.0 OR MIT" +repository = "https://github.com/google/zerocopy" +rust-version = "1.65.0" + +exclude = [".*"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"] diff --git a/unsafe-fields/LICENSE-APACHE b/unsafe-fields/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/unsafe-fields/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/unsafe-fields/LICENSE-BSD b/unsafe-fields/LICENSE-BSD new file mode 120000 index 0000000000..d37ba4277e --- /dev/null +++ b/unsafe-fields/LICENSE-BSD @@ -0,0 +1 @@ +../LICENSE-BSD \ No newline at end of file diff --git a/unsafe-fields/LICENSE-MIT b/unsafe-fields/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/unsafe-fields/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/unsafe-fields/src/lib.rs b/unsafe-fields/src/lib.rs new file mode 100644 index 0000000000..af8ae5ed3e --- /dev/null +++ b/unsafe-fields/src/lib.rs @@ -0,0 +1,356 @@ +// Copyright 2024 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Support for unsafe fields. +//! +//! This crate provides the [`unsafe_fields!`] macro, which can be used to mark +//! fields as unsafe. Unsafe fields automatically have their types wrapped using +//! the [`Unsafe`] wrapper type. An `Unsafe` is intended to be used to for +//! struct, enum, or union fields which carry safety invariants. All accessors +//! are `unsafe`, which requires any use of an `Unsafe` field to be inside an +//! `unsafe` block. +//! +//! An unsafe field has the type `Unsafe`. `O` is +//! the enclosing type (struct, enum, or union), `F` is the type of the field, +//! and `NAME_HASH` is the hash of the field's name. `O` prevents swapping +//! unsafe fields of the same `F` type between different enclosing types, and +//! `NAME_HASH` prevents swapping different fields of the same `F` type within +//! the same enclosing type. Note that swapping the same field between instances +//! of the same type [cannot be prevented](crate#limitations). +//! +//! # Examples +//! +//! ``` +//! use unsafe_fields::{unsafe_fields, Unsafe}; +//! +//! unsafe_fields! { +//! /// A `usize` which is guaranteed to be even. +//! pub struct EvenUsize { +//! // INVARIANT: `n` is even. +//! #[unsafe] +//! n: usize, +//! } +//! } +//! +//! impl EvenUsize { +//! /// Constructs a new `EvenUsize`. +//! /// +//! /// Returns `None` if `n` is odd. +//! pub fn new(n: usize) -> Option { +//! if n % 2 != 0 { +//! return None; +//! } +//! // SAFETY: We just confirmed that `n` is even. +//! let n = unsafe { Unsafe::new(n) }; +//! Some(EvenUsize { n }) +//! } +//! } +//! ``` +//! +//! Attempting to swap unsafe fields of the same type is prevented: +//! +//! ```compile_fail,E0308 +//! use unsafe_fields::{unsafe_fields, Unsafe}; +//! +//! unsafe_fields! { +//! /// A range. +//! pub struct Range { +//! // INVARIANT: `lo <= hi`. +//! #[unsafe] +//! lo: usize, +//! #[unsafe] +//! hi: usize, +//! } +//! } +//! +//! impl Range { +//! pub fn swap(&mut self) { +//! // ERROR: Mismatched types +//! core::mem::swap(&mut self.lo, &mut self.hi); +//! } +//! } +//! ``` +//! +//! # Limitations +//! +//! Note that we cannot prevent `Unsafe`s from being swapped between the same +//! field in instances of the same type: +//! +//! ``` +//! use unsafe_fields::{unsafe_fields, Unsafe}; +//! +//! unsafe_fields! { +//! /// A `usize` which is guaranteed to be even. +//! pub struct EvenUsize { +//! // INVARIANT: `n` is even. +//! #[unsafe] +//! n: usize, +//! } +//! } +//! +//! pub fn swap(a: &mut EvenUsize, b: &mut EvenUsize) { +//! core::mem::swap(&mut a.n, &mut b.n); +//! } +//! ``` + +use core::marker::PhantomData; + +/// A field with safety invariants. +/// +/// `Unsafe` should not be named directly - instead, use [`unsafe_fields!`] to +/// declare a type with unsafe fields. +/// +/// See the [crate-level documentation](crate) for more information. +#[repr(transparent)] +pub struct Unsafe { + _marker: PhantomData, + // INVARIANT: `field` is only modified via public `unsafe` methods. User code is never + // invoked implicitly except via public `unsafe` methods. + field: F, +} + +// NOTE on design: It may seem counter-intuitive to offer an impl of traits that +// don't require `unsafe` to call. Unfortunately, this is a fundamental +// requirement if users want to be able to mark their types as `Copy`. Luckily, +// we can implement `Copy` (and its unavoidable super-trait, `Clone`) without +// invoking user code or opening up the possibility of modifying the field. We +// do this by only implementing `Copy` and `Clone` when `F: Copy`. For `Clone`, +// the user is still able to provide a manual impl, so this does not +// fundamentally restrict what behavior can be supported. +impl Copy for Unsafe {} +impl Clone for Unsafe { + #[allow(clippy::non_canonical_clone_impl)] + fn clone(&self) -> Self { + // SAFETY: We don't call any user-defined code here (only make a + // bit-for-bit copy of `self.field`), so there's no way to accidentally + // invoke user-defined code or modify `self.field`. + Unsafe { _marker: PhantomData, field: self.field } + } +} + +impl Unsafe { + /// Gets a reference to the inner value. + /// + /// # Safety + /// + /// The caller is responsible for upholding any safety invariants associated + /// with this field. + #[inline(always)] + pub const unsafe fn as_ref(&self) -> &F { + // SAFETY: This method is unsafe to call. + &self.field + } + + /// Gets a mutable reference to the inner value. + /// + /// # Safety + /// + /// The caller is responsible for upholding any safety invariants associated + /// with this field. + #[inline(always)] + pub unsafe fn as_mut(&mut self) -> &mut F { + // SAFETY: This method is unsafe to call. + &mut self.field + } +} + +impl Unsafe { + /// Constructs a new `Unsafe`. + /// + /// # Safety + /// + /// The caller is responsible for upholding any safety invariants associated + /// with this field. + #[inline(always)] + pub const unsafe fn new(field: F) -> Unsafe { + // SAFETY: This method is unsafe to call. + Unsafe { _marker: PhantomData, field } + } + + /// Extracts the inner `F` from `self`. + /// + /// # Safety + /// + /// The caller is responsible for upholding any safety invariants associated + /// with this field. + #[inline(always)] + pub const unsafe fn into(self) -> F { + use core::mem::ManuallyDrop; + + let slf = ManuallyDrop::new(self); + + #[repr(C)] + union Transmute { + src: ManuallyDrop>, + dst: ManuallyDrop, + } + + // SAFETY: `ManuallyDrop>` has the same size and bit + // validity as `Unsafe<_, F, _>`. [1] `Unsafe<_, F, _>` is + // `#[repr(transparent)]` and has no other fields, and so it has the + // same size and bit validity as `F`. + // + // [1] Per https://doc.rust-lang.org/1.81.0/core/mem/struct.ManuallyDrop.html: + // + // `ManuallyDrop` is guaranteed to have the same layout and bit + // validity as `T` + let dst = unsafe { Transmute { src: slf }.dst }; + + // SAFETY (satisfaction of `Unsafe`'s field invariant): This method is + // unsafe to call. + ManuallyDrop::into_inner(dst) + } +} + +/// Defines a type with unsafe fields. +/// +/// See the [crate-level documentation](crate) for more information. +// TODO: Allow specifying *which* fields are unsafe. +#[macro_export] +macro_rules! unsafe_fields { + ($(#[$attr:meta])* $vis:vis struct $name:ident { + $($(#[$field_attr:tt])? $field:ident: $field_ty:ty),* $(,)? + }) => { + $(#[$attr])* + $vis struct $name { + $( + $field: unsafe_fields!(@field $(#[$field_attr])? $field: $field_ty), + )* + } + }; + (@field #[unsafe] $field:ident: $field_ty:ty) => { + $crate::Unsafe + }; + (@field $_field:ident: $field_ty:ty) => { + $field_ty + } +} + +#[doc(hidden)] +pub mod macro_util { + // TODO: Implement a stronger hash function so we can basically just ignore + // collisions. If users encounter collisions in practice, we can just deal + // with it then, publish a new version, and tell them to upgrade. + pub const fn hash_field_name(field_name: &str) -> u128 { + // An implementation of FxHasher, although returning a u128. Probably + // not as strong as it could be, but probably more collision resistant + // than normal 64-bit FxHasher. + let field_name = field_name.as_bytes(); + let mut hash = 0u128; + let mut i = 0; + while i < field_name.len() { + // This is just FxHasher's `0x517cc1b727220a95` constant + // concatenated back-to-back. + const K: u128 = 0x517cc1b727220a95517cc1b727220a95; + hash = (hash.rotate_left(5) ^ (field_name[i] as u128)).wrapping_mul(K); + i += 1; + } + hash + } +} + +#[cfg(test)] +mod tests { + use super::*; + + unsafe_fields! { + /// A `Foo`. + #[allow(unused)] + struct Foo { + #[unsafe] + a: usize, + b: usize, + } + } + + unsafe_fields! { + /// A `Bar`. + #[allow(unused)] + struct Bar { + #[unsafe] + a: usize, + #[unsafe] + b: usize, + } + } + + #[test] + fn test_unsafe_fieds() { + let mut _foo = Foo { a: unsafe { Unsafe::new(0) }, b: 0 }; + let mut _bar = Bar { a: unsafe { Unsafe::new(0) }, b: unsafe { Unsafe::new(0) } }; + } +} + +/// This module exists so that we can use rustdoc to perform compile-fail tests +/// rather than having to set up an entire trybuild set suite. +/// +/// ```compile_fail,E0308 +/// use unsafe_fields::*; +/// +/// unsafe_fields! { +/// struct Foo { +/// #[unsafe] +/// a: usize, +/// b: usize, +/// } +/// } +/// +/// impl Foo { +/// // Swapping an unsafe field with a non-unsafe field is a compile error. +/// fn swap(&mut self) { +/// core::mem::swap(&mut self.a, &mut self.b); +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0308 +/// use unsafe_fields::*; +/// +/// unsafe_fields! { +/// struct Foo { +/// #[unsafe] +/// a: usize, +/// #[unsafe] +/// b: usize, +/// } +/// } +/// +/// impl Foo { +/// // Swapping an unsafe field with another unsafe field is a compile +/// // error. +/// fn swap(&mut self) { +/// core::mem::swap(&mut self.a, &mut self.b); +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0308 +/// use unsafe_fields::*; +/// +/// unsafe_fields! { +/// struct Foo { +/// #[unsafe] +/// a: usize, +/// } +/// } +/// +/// unsafe_fields! { +/// struct Bar { +/// #[unsafe] +/// a: usize, +/// } +/// } +/// +/// // Swapping identically-named unsafe fields from different types is a +/// // compile error. +/// fn swap(foo: &mut Foo, bar: &mut Bar) { +/// core::mem::swap(&mut foo.a, &mut bar.a); +/// } +/// ``` +#[doc(hidden)] +pub mod compile_fail {}