Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tools/cargo-zerocopy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ fn set_ui_test_feature_args(command: &mut Command, args: &[String]) {
fn get_rustflags(name: &str) -> String {
// See #1792 for context on zerocopy_derive_union_into_bytes.
let mut flags =
"--cfg zerocopy_unstable_linux --cfg zerocopy_derive_union_into_bytes --cfg __ZEROCOPY_INTERNAL_USE_ONLY_DEV_MODE"
"--cfg zerocopy_unstable_linux --cfg zerocopy_unstable_ptr --cfg zerocopy_derive_union_into_bytes --cfg __ZEROCOPY_INTERNAL_USE_ONLY_DEV_MODE"
.to_string();
flags += &format!(" --cfg __ZEROCOPY_INTERNAL_USE_ONLY_TOOLCHAIN=\"{name}\"");

Expand Down
94 changes: 79 additions & 15 deletions zerocopy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,62 @@ pub unsafe trait Immutable {
/// }
/// ```
///
#[cfg_attr(
zerocopy_unstable_ptr,
doc = r#"
# Field invariants

This experimental feature requires `--cfg zerocopy_unstable_ptr`.

Named fields of structs, enum variants, and unions can specify additional
runtime checks using `#[zerocopy(invariant(expression))]`:

```
# use zerocopy_derive::TryFromBytes;
#[derive(TryFromBytes)]
struct Foo {
a: u8,
#[zerocopy(invariant((**a.unaligned_as_ref() % 2) == (**b.unaligned_as_ref() as u8)))]
b: bool,
#[zerocopy(invariant(**c.unaligned_as_ref() > 0))]
c: i8,
}
```

Each expression must return a `bool`. It has access to validated, read-only
[`Ptr`]s to the current field and all preceding fields of the struct or
variant, using their field names. A union's invariants have access only to
the current field. The expression can use the existing [`Ptr`] APIs to
inspect those fields. In this example, all fields are accessed by reference.

Rust's usual restrictions on local bindings apply; for example, a field name
cannot shadow an in-scope constant.

Fields are checked in declaration order. Each field's bit validity is
checked before its invariants run. Multiple invariants on a field run in
attribute order. For structs and enums, validation stops at the first invalid
field or invariant that returns `false`. For unions, a failed bit-validity
check or invariant causes validation to try the next field; validation
succeeds as soon as one field and all its invariants pass. Each expression
runs in its own closure; `return` returns from that expression, and panics
propagate to the caller. Only the selected enum variant's fields and
invariants are checked. Expressions may have arbitrary side effects, even
when the conversion fails or its result is discarded.

These predicates are additional acceptance checks beyond Rust's bit validity;
bit-valid bytes may still be rejected. See [What is a "valid instance"?] for
the distinction.

[What is a "valid instance"?]: trait@TryFromBytes#what-is-a-valid-instance

Invariants are not supported on tuple fields. Types with invariants cannot
derive [`FromZeros`] or [`FromBytes`], whose conversions do not perform runtime
validation. These checks apply to conversions through [`TryFromBytes`]; they
do not restrict ordinary construction or mutation of Rust values.

"#
)]
///
/// # Portability
///
/// To ensure consistent endianness for enums with multi-byte representations,
Expand Down Expand Up @@ -1802,6 +1858,14 @@ pub use zerocopy_derive::TryFromBytes;
/// If you are negatively affected by lack of support for a particular type,
/// we encourage you to let us know by [filing an issue][github-repo].
///
/// In this trait's conversion methods, a "valid instance" must also pass any
/// configured field invariants, including those on nested fields. Rust bit
/// validity alone does not guarantee that a conversion succeeds: an invariant
/// may reject otherwise bit-valid bytes. Such predicates check acceptance at
/// conversion time; they do not constrain subsequent mutation or ordinary Rust
/// construction. See the [derive's field invariants][derive] documentation for
/// the experimental attribute's syntax, evaluation order, and side effects.
///
/// # `TryFromBytes` is not symmetrical with [`IntoBytes`]
///
/// There are some types which implement both `TryFromBytes` and [`IntoBytes`],
Expand Down Expand Up @@ -1966,7 +2030,7 @@ pub unsafe trait TryFromBytes {
@variant "dynamic_padding"
]
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_ref_from_bytes(source: &[u8]) -> Result<&Self, TryCastError<&[u8], Self>>
Expand Down Expand Up @@ -2089,7 +2153,7 @@ pub unsafe trait TryFromBytes {
@variant "dynamic_padding"
]
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_ref_from_prefix(source: &[u8]) -> Result<(&Self, &[u8]), TryCastError<&[u8], Self>>
Expand Down Expand Up @@ -2199,7 +2263,7 @@ pub unsafe trait TryFromBytes {
@variant "dynamic_padding"
]
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_ref_from_suffix(source: &[u8]) -> Result<(&[u8], &Self), TryCastError<&[u8], Self>>
Expand Down Expand Up @@ -2293,7 +2357,7 @@ pub unsafe trait TryFromBytes {
#[doc = codegen_header!("h5", "try_mut_from_bytes")]
///
/// See [`TryFromBytes::try_ref_from_bytes`](#method.try_ref_from_bytes.codegen).
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_mut_from_bytes(bytes: &mut [u8]) -> Result<&mut Self, TryCastError<&mut [u8], Self>>
Expand Down Expand Up @@ -2402,7 +2466,7 @@ pub unsafe trait TryFromBytes {
#[doc = codegen_header!("h5", "try_mut_from_prefix")]
///
/// See [`TryFromBytes::try_ref_from_prefix`](#method.try_ref_from_prefix.codegen).
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_mut_from_prefix(
Expand Down Expand Up @@ -2502,7 +2566,7 @@ pub unsafe trait TryFromBytes {
#[doc = codegen_header!("h5", "try_mut_from_suffix")]
///
/// See [`TryFromBytes::try_ref_from_suffix`](#method.try_ref_from_suffix.codegen).
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_mut_from_suffix(
Expand Down Expand Up @@ -2607,7 +2671,7 @@ pub unsafe trait TryFromBytes {
@variant "dynamic_padding"
]
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_ref_from_bytes_with_elems(
Expand Down Expand Up @@ -2728,7 +2792,7 @@ pub unsafe trait TryFromBytes {
@variant "dynamic_padding"
]
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_ref_from_prefix_with_elems(
Expand Down Expand Up @@ -2836,7 +2900,7 @@ pub unsafe trait TryFromBytes {
@variant "dynamic_padding"
]
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_ref_from_suffix_with_elems(
Expand Down Expand Up @@ -2932,7 +2996,7 @@ pub unsafe trait TryFromBytes {
#[doc = codegen_header!("h5", "try_mut_from_bytes_with_elems")]
///
/// See [`TryFromBytes::try_ref_from_bytes_with_elems`](#method.try_ref_from_bytes_with_elems.codegen).
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_mut_from_bytes_with_elems(
Expand Down Expand Up @@ -3043,7 +3107,7 @@ pub unsafe trait TryFromBytes {
#[doc = codegen_header!("h5", "try_mut_from_prefix_with_elems")]
///
/// See [`TryFromBytes::try_ref_from_prefix_with_elems`](#method.try_ref_from_prefix_with_elems.codegen).
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_mut_from_prefix_with_elems(
Expand Down Expand Up @@ -3143,7 +3207,7 @@ pub unsafe trait TryFromBytes {
#[doc = codegen_header!("h5", "try_mut_from_suffix_with_elems")]
///
/// See [`TryFromBytes::try_ref_from_suffix_with_elems`](#method.try_ref_from_suffix_with_elems.codegen).
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_mut_from_suffix_with_elems(
Expand Down Expand Up @@ -3210,7 +3274,7 @@ pub unsafe trait TryFromBytes {
bench = "try_read_from_bytes",
format = "coco_static_size",
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_read_from_bytes(source: &[u8]) -> Result<Self, TryReadError<&[u8], Self>>
Expand Down Expand Up @@ -3288,7 +3352,7 @@ pub unsafe trait TryFromBytes {
bench = "try_read_from_prefix",
format = "coco_static_size",
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_read_from_prefix(source: &[u8]) -> Result<(Self, &[u8]), TryReadError<&[u8], Self>>
Expand Down Expand Up @@ -3367,7 +3431,7 @@ pub unsafe trait TryFromBytes {
bench = "try_read_from_suffix",
format = "coco_static_size",
)]
#[must_use = "has no side effects"]
#[must_use = "the conversion result must be checked"]
#[cfg_attr(zerocopy_inline_always, inline(always))]
#[cfg_attr(not(zerocopy_inline_always), inline)]
fn try_read_from_suffix(source: &[u8]) -> Result<(&[u8], Self), TryReadError<&[u8], Self>>
Expand Down
2 changes: 1 addition & 1 deletion zerocopy/src/pointer/ptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ mod _transitions {
}

/// Casts of the referent type.
#[cfg_attr(not(zerocopy_unstable_ptr), allow(unreachable_pub))]
#[allow(unreachable_pub)] // False positive on MSRV
pub use _casts::TryWithError;
mod _casts {
use core::cell::UnsafeCell;
Expand Down
10 changes: 6 additions & 4 deletions zerocopy/testutil/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,16 @@ impl UiTestRunner {

let mut command = Command::new("cargo");
command.current_dir(workspace_root.clone());
// We strip `--cfg zerocopy_derive_union_into_bytes` and `--cfg
// zerocopy_unstable_linux` from `RUSTFLAGS` so that the
// We strip experimental feature cfgs from `RUSTFLAGS` so that the
// `zerocopy-derive` proc macro is built without them. This ensures it
// generates the feature-gate checks into the UI tests, which we can
// then explicitly enable or disable via `rustc_args`.
let mut rustflags = env::var("RUSTFLAGS").unwrap_or_default();
let cfgs_to_strip =
["--cfg zerocopy_derive_union_into_bytes", "--cfg zerocopy_unstable_linux"];
let cfgs_to_strip = [
"--cfg zerocopy_derive_union_into_bytes",
"--cfg zerocopy_unstable_linux",
"--cfg zerocopy_unstable_ptr",
];
for &cfg in &cfgs_to_strip {
rustflags = rustflags.replace(cfg, "");
}
Expand Down
1 change: 1 addition & 0 deletions zerocopy/zerocopy-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(coverage_nightly)',
'cfg(zerocopy_derive_union_into_bytes)',
'cfg(zerocopy_unstable_linux)',
'cfg(zerocopy_unstable_ptr)',
] }

[lib]
Expand Down
12 changes: 12 additions & 0 deletions zerocopy/zerocopy-derive/src/derive/from_bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ pub(crate) fn find_zero_variant(enm: &DataEnum) -> Result<usize, bool> {
Err(has_unknown_discriminants)
}
pub(crate) fn derive_from_zeros(ctx: &Ctx, top_level: Trait) -> Result<TokenStream, Error> {
if let Some(span) = ctx.invariant_span {
return ctx.error_or_skip(Error::new(
span,
"cannot derive `FromZeros` for a type with invariants",
));
}
let try_from_bytes = derive_try_from_bytes(ctx, top_level)?;
let from_zeros = match &ctx.ast.data {
Data::Struct(strct) => derive_from_zeros_struct(ctx, strct),
Expand All @@ -98,6 +104,12 @@ pub(crate) fn derive_from_zeros(ctx: &Ctx, top_level: Trait) -> Result<TokenStre
Ok(IntoIterator::into_iter([try_from_bytes, from_zeros]).collect())
}
pub(crate) fn derive_from_bytes(ctx: &Ctx, top_level: Trait) -> Result<TokenStream, Error> {
if let Some(span) = ctx.invariant_span {
return ctx.error_or_skip(Error::new(
span,
"cannot derive `FromBytes` for a type with invariants",
));
Comment thread
jswrenn marked this conversation as resolved.
}
let from_zeros = derive_from_zeros(ctx, top_level)?;
let from_bytes = match &ctx.ast.data {
Data::Struct(strct) => derive_from_bytes_struct(ctx, strct),
Expand Down
Loading
Loading