From eda1edf292ea04e9a838b4fdac863a0ecb161714 Mon Sep 17 00:00:00 2001 From: Josh Liebow-Feeser Date: Fri, 2 Oct 2026 11:05:49 -0400 Subject: [PATCH] Prove cast validation and exact-size metadata inference Specify metadata_for_exact_size and validate_cast_and_convert_metadata against independent layout calculations. Prove exact representability, greatest-fitting metadata, alignment-before-size error priority and the complete prefix or suffix result. Add Rust assertion harnesses comparing the full optional metadata and cast outcomes with remainder-based checked arithmetic. Keep address-addition bounds and the zero-stride panic exclusion visible in their domains. Outcome examples cover unattainable sizes and intentional panic separately from checks of the specification's meaning. Prove the concrete u32 slice KnownLayout initializer and every checked size_for_elems result through a Rust assertion harness. Admission traverses the actual local constant body, rather than assuming its descriptor. An initializer mutation challenges that connection. Retain reproducible native capability fixtures for generic KnownLayout, the production transmute macro, and raw pointers. Unsupported equalities, recursive declaration dependencies, and incomplete pointer carriers remain explicit blockers; no bounds or declarations are removed to admit them. gherrit-pr-id: Grerckvocdnwpdtef665ubpieo6wlwfdj Agent-authored-by: AI agent acting on joshlf's behalf --- verification/aeneas/DESIGN.md | 99 +++++- verification/aeneas/README.md | 4 + verification/aeneas/golden/Funs.lean | 306 +++++++++++++++++- verification/aeneas/golden/Types.lean | 15 + verification/aeneas/lean/Corollaries.lean | 11 + verification/aeneas/lean/LayoutModel.lean | 49 ++- verification/aeneas/lean/MathViews.lean | 26 +- verification/aeneas/lean/Obligations.lean | 63 ++++ .../aeneas/lean/Obligations/TailChecks.lean | 11 + verification/aeneas/lean/OutcomeTests.lean | 124 +++++++ .../aeneas/lean/Proofs/Contracts.lean | 45 +++ .../aeneas/lean/Proofs/PrimitiveChecks.lean | 72 +++++ verification/aeneas/lean/Proofs/Raw/Cast.lean | 283 ++++++++++++++++ .../aeneas/lean/Proofs/TailChecks.lean | 156 +++++++++ .../aeneas/lean/Proofs/TailReference.lean | 248 ++++++++++++++ .../aeneas/lean/RequiredModelContracts.lean | 44 +++ .../RequiredModelContracts/TailChecks.lean | 18 ++ .../aeneas/projections/ModelShapes.lean | 4 + verification/aeneas/projections/Models.lean | 6 + verification/aeneas/projections/Specs.lean | 36 +++ .../aeneas/tests/capabilities/README.md | 107 ++++++ .../capabilities/invalid-transmute/Cargo.lock | 61 ++++ .../capabilities/invalid-transmute/Cargo.toml | 12 + .../capabilities/invalid-transmute/src/lib.rs | 11 + .../capabilities/known-layout/Cargo.lock | 61 ++++ .../capabilities/known-layout/Cargo.toml | 12 + .../capabilities/known-layout/src/lib.rs | 25 ++ .../capabilities/owned-transmute/Cargo.lock | 61 ++++ .../capabilities/owned-transmute/Cargo.toml | 12 + .../capabilities/owned-transmute/src/lib.rs | 15 + .../aeneas/tests/capabilities/probe.py | 97 ++++++ .../capabilities/raw-pointers/Cargo.lock | 61 ++++ .../capabilities/raw-pointers/Cargo.toml | 12 + .../capabilities/raw-pointers/src/lib.rs | 21 ++ .../capabilities/std-transmute/Cargo.lock | 7 + .../capabilities/std-transmute/Cargo.toml | 9 + .../capabilities/std-transmute/src/lib.rs | 20 ++ zerocopy/src/layout/mod.rs | 14 + zerocopy/src/layout/primitive_checks.rs | 42 +++ zerocopy/src/layout/tail_checks.rs | 287 +++++++++++++++- 40 files changed, 2555 insertions(+), 12 deletions(-) create mode 100644 verification/aeneas/lean/OutcomeTests.lean create mode 100644 verification/aeneas/lean/Proofs/Raw/Cast.lean create mode 100644 verification/aeneas/tests/capabilities/README.md create mode 100644 verification/aeneas/tests/capabilities/invalid-transmute/Cargo.lock create mode 100644 verification/aeneas/tests/capabilities/invalid-transmute/Cargo.toml create mode 100644 verification/aeneas/tests/capabilities/invalid-transmute/src/lib.rs create mode 100644 verification/aeneas/tests/capabilities/known-layout/Cargo.lock create mode 100644 verification/aeneas/tests/capabilities/known-layout/Cargo.toml create mode 100644 verification/aeneas/tests/capabilities/known-layout/src/lib.rs create mode 100644 verification/aeneas/tests/capabilities/owned-transmute/Cargo.lock create mode 100644 verification/aeneas/tests/capabilities/owned-transmute/Cargo.toml create mode 100644 verification/aeneas/tests/capabilities/owned-transmute/src/lib.rs create mode 100644 verification/aeneas/tests/capabilities/probe.py create mode 100644 verification/aeneas/tests/capabilities/raw-pointers/Cargo.lock create mode 100644 verification/aeneas/tests/capabilities/raw-pointers/Cargo.toml create mode 100644 verification/aeneas/tests/capabilities/raw-pointers/src/lib.rs create mode 100644 verification/aeneas/tests/capabilities/std-transmute/Cargo.lock create mode 100644 verification/aeneas/tests/capabilities/std-transmute/Cargo.toml create mode 100644 verification/aeneas/tests/capabilities/std-transmute/src/lib.rs diff --git a/verification/aeneas/DESIGN.md b/verification/aeneas/DESIGN.md index b9aa5b58cf..b1b28560ae 100644 --- a/verification/aeneas/DESIGN.md +++ b/verification/aeneas/DESIGN.md @@ -304,6 +304,92 @@ the library already models every part of Rust's abstract machine. ## Deferred decisions and reopening evidence +### Resource-free layout and storage work + +The shared library supplies two independent foundations for layout and owned +byte-cast proofs: + +- `Rust.Pointer.Value` retains an address, opaque optional provenance, and + metadata. Its casts and metadata operations preserve the exact provenance + value. Pointer equality observes address and metadata; it does not compare + provenance. Non-nullness supplies no alignment, liveness, or access permission. +- `Rust.Storage.StorageDecoder` takes metadata explicitly, checks the requested + byte width, and checks that a successful raw result carries that metadata. + Fixed-width decoders adapt through unit metadata. Slice counts are inputs, + including for zero-sized elements; byte length cannot recover those counts. + +These are mathematical definitions and checked laws, not newly admitted Rust +operations. The storage carrier still contains only initialized numeric bytes +and uninitialized bytes. It does not encode pointers or decide where provenance +lives in a pointer's representation. Pointer-bearing byte operations remain +unsupported. Typed copies still require the treatment described in +[the lowering audit](LOWERING_AUDIT.md), including +[Aeneas issue #1405](https://github.com/AeneasVerif/aeneas/issues/1405). + +Byte-cast reasoning uses the existing distinction between language validity and +optional mathematical admission. The source's `IntoBytes` law ranges over all +accepted language representations, regardless of mathematical admission. The +destination's intended `FromBytes` law must accept every initialized storage of +the correct width at both stages. The shared cast lemma composes these laws for +arbitrary represented bytes. It supplies no encoder and does not claim that a +particular Rust transmutation follows this interpretation. + +The concrete `check_u32_slice_known_layout` Rust harness checks the actual +`<[u32] as KnownLayout>::LAYOUT` initializer, its descriptor fields, and sizing +for every element count, including checked-multiplication overflow. Admission +visits the local constant's initializer body; no axiom supplies the descriptor. +The visible guards expose the power-of-two and size-divisibility premises of +the ABI reads. This checks that concrete numerical behavior, not the complete +generic trait invariant. The proof also rejects an initializer changed from +`for_slice` to `for_type`. + +The native capability fixtures under `tests/capabilities/` preserve the next +translation boundaries. With the current pins, the generic `KnownLayout` +fixture retains an associated-type equality that Aeneas rejects. A second +fixture without an explicit caller-side equality fails on the trait's own +`MaybeUninit` metadata equality. The production +`transmute!` fixture retains its compile-time trait-check helper and a loop that +Aeneas rejects. The required `FromBytes` declaration dependencies additionally +reach the unsupported recursive `Invariants`/`Alignment` group. These failures +are not successful extraction or completed Rust proofs. Do not delete bounds, +equalities, or required declarations to make those targets extract. Required +recursive declaration support is deferred; dependent work must wait for tools +that support it. + +Native raw-pointer emission has a separate limitation. The pinned backend's +carrier stores only `v : T`, and its scalar pointer cast returns `undef`. +Emitted slice construction and `NonNull` operations remain external templates. +The mathematical pointer model does not repair that carrier or grant admission +to those operations. The existing checker continues to reject them. A faithful +carrier and checked primitive interpretations are prerequisites for proving +their address, metadata, or provenance behavior. + +When those boundaries are supported, bind `KnownLayout::LAYOUT` to the original +Rust type and its metadata-indexed geometry. Reuse the complete recursive +layout semantics, which preserves an inner field's padding before outer +rounding. Preserve physical tail offset, stride, alignment, and conservative +padding flags. Keep mathematical extent, `usize` representability, and the +`isize` object-size restriction separate. The `MaybeUninit` obligation also +requires identical metadata and layout and acceptance of uninitialized storage +in every position; a layout-only theorem must not be presented as the full +trait invariant. + +Concrete owned conversions use the actual standard transmute primitive for +`u32` and four-byte arrays, plus a guarded `u8`-to-`bool` conversion. These types +have no pointers, resources, destruction, or external-state invariants. Original +Rust endpoint checks and a compiled canonical-provider audit restrict that +interpretation to those cases; it does not admit the production macro or an +arbitrary generic transmute. Invalid Boolean encodings take the forbidden path, +even with postcondition `True`. Reference creation remains a later target because +numerical geometry and provenance presence alone do not supply permissions or +lifetimes. Derive +proof requests are also deferred. Existing annotation discovery, native binding +records, independent golden/live proofs, and dependency audits remain the sole +route to reporting a Rust specification as checked; these foundations add no +parallel coverage registry or automatic Rust ABI assumptions. + +### Other deferred decisions + - **Canonical layout abstraction.** Retain structural `DstLayout` and existing `ModelViews` initially. Padding and trailing-size pilots already use those views. Use `extend`, sequence queries, and capacity contracts to compare total @@ -325,8 +411,8 @@ the library already models every part of Rust's abstract machine. tactics, and the existing loop adapter. Introduce a larger lifting DSL, per-model tactics, or new loop abstraction only after concrete repeated failures show a net benefit over ordinary lemmas and a thin tactic. -- **Broader carriers and effects.** Recursive nominal groups, indexed carriers, - const generics, associated-type trait laws, higher-ranked bounds, bounded +- **Broader carriers and effects.** Recursive nominal groups, general indexed + carriers, const generics, associated-type trait laws, higher-ranked bounds, bounded nominal models, and escaping borrow reconstruction remain unsupported with explicit errors. The first trait-law bridge supports `Self`-only traits and simple generic bounds, with `Unaligned` and a total byte codec as checked @@ -338,11 +424,12 @@ the library already models every part of Rust's abstract machine. execution, independently of ordinary library-model admission. A storage model must account for the translation's omissions as well as its primitives. The [pinned lowering audit](LOWERING_AUDIT.md) records known omissions and the - additional admission checks needed before making memory-safety claims. - [ADMISSION_DESIGN.md](ADMISSION_DESIGN.md) specifies the proposed closed + admission checks needed before making memory-safety claims. + [ADMISSION_DESIGN.md](ADMISSION_DESIGN.md) specifies the closed dependency traversal, controlled build origins using Charon's existing MIR - preservation setup, primitive rules, and mandatory CI path. These additional - checks are not implemented yet. + preservation setup, primitive rules, and mandatory CI path. CI enforces + these checks for the admitted fragment; extending that fragment requires + corresponding operation semantics and coverage checks. + +# Native capability fixtures + +These small, independent crates challenge the pinned native extraction without +editing compiler output, deleting associated equalities, or replacing local +helpers with axioms. They are capability probes, not admitted Rust soundness +proofs. Each new observation records the repository's actual HEAD, dirty status, +source digests, and validated runtime metadata. + +Run from the repository root with the existing `AENEAS_TOOLCHAIN_DIR` and +`CARGO_HOME` from the verification setup, using a fresh evidence directory: + +```bash +python3 -B verification/aeneas/tests/capabilities/probe.py known-layout \ + --runtime "$AENEAS_TOOLCHAIN_DIR" \ + --cargo-home "$CARGO_HOME" \ + --work target/capabilities/known-layout-new \ + --cargo-target target/owned-transmute-capability/cargo-target +``` + +Replace the case and work directory with `owned-transmute`, `invalid-transmute`, +`raw-pointers`, `std-transmute`, `known-layout-unconstrained`, or +`known-layout-concrete`. +The last two select only their own root from the `known-layout` fixture. +Each run records commands, exit codes, source digest, and +logs in `evidence.json`. Charon also records the pre-transformation ULLBC and +final LLBC. The driver refuses an existing evidence directory and returns the +first failing stage's status. Inspect the recorded stage results to distinguish +the expected negative fixture from an unexpected failure. +The tracked Cargo lockfiles are used with `--locked`; the probe does not +regenerate them. Compilation occurs through the native Charon driver, as in the +verification pipeline. + +Observed with the preserved patched runtime on October 14, 2026: + +| Case | Charon | Aeneas | Result | +| --- | --- | --- | --- | +| `known-layout` | 0 | 2 | The original `KnownLayout` constraint survives lifting. `SymbolicToPureTypes.ml:1000` requires an empty associated-type constraint list and rejects the generic root signature. | +| `known-layout-unconstrained` | 0 | 2 | The generic root has no associated-type constraints. The original trait's `MaybeUninit` metadata equality survives lifting and triggers the same check while Aeneas translates the trait's method signatures. | +| `known-layout-concrete` | 0 | 0 | The concrete slice root emits, but calls an external associated-constant axiom template; its descriptor value or initializer is not translated or proved. | +| `owned-transmute` | 0 | 2 | The actual production `transmute!` expansion retains its compile-time trait-check helper. `InterpLoops.ml:411` rejects that helper's infinite loop. No conversion theorem is established. | +| `invalid-transmute` | 101 | Not run | Rust rejects `u8 -> bool` because `bool` does not implement `FromBytes` (E0277). This confirms a compilation gate, not a runtime byte-decoder proof. | +| `raw-pointers` | 0 | 0 | Native emission succeeds, but does not establish useful raw-pointer semantics or an admitted proof. | + +The raw-pointer result needs particular care. The pinned backend's +`Aeneas/Std/RawPtr.lean` represents a raw pointer by `v : T`; it carries no +address or provenance. Its `RawPtr.cast_scalar` always returns `.fail .undef`. +The emitted slice-pointer constructor and `NonNull::new` are external function +axiom templates, and `NonNull` is an external type axiom template. None of those +templates is an accepted interpretation in this experiment. The existing +admission checker also rejects raw-pointer types. Successful emission therefore +does not provide a pointer-preservation or memory-safety theorem. + +The exact `KnownLayout` experiment fails before recursive trait extraction. +Its error must not be relabeled as a recursive-declaration diagnostic. The +trait's recursive `MaybeUninit: KnownLayout` bound and metadata equality remain +in the preserved original compiler evidence; no translation patch or weakened +trait was introduced. + +The unconstrained follow-up isolates that retained trait equality from the +explicit equality in the first root. Its final trait constraint identifies +`::PointerMetadata = Self::PointerMetadata`. +The concrete follow-up emits a call to +`Slice.Insts.ZerocopyKnownLayout.LAYOUT Std.U32`, with the implementation's +associated constant left in `FunsExternal_Template.lean` as an axiom because +zerocopy is a dependency of the independent fixture. This emission cannot stand +in for a proof of the constant's body or of a generic `KnownLayout` dictionary. + +## Checked local initializer + +The production harness `check_u32_slice_known_layout` in +`zerocopy/src/layout/primitive_checks.rs` compiles inside zerocopy's own crate. +Unlike the independent dependency fixture, it reaches the local slice +implementation's actual associated-constant initializer. The preserved and +final compiler evidence retains that body; admission traverses its execution. +Native output defines `Slice.Insts.ZerocopyKnownLayout.LAYOUT T` by calling +`layout.DstLayout.for_slice T`. No external axiom supplies the descriptor. + +The harness checks the actual `[u32]` descriptor fields and its size for every +count against independent checked multiplication. Its Lean proof uses that +retained initializer under the visible ABI-read guards. A development negative +control changes the initializer from `for_slice` to `for_type`: the native module +still elaborates, but the proof fails. The associated constant is thus part of +the checked behavior, rather than an assumed trait promise. + +This remains a concrete numerical assertion proof under the existing primitive +interpretations and Rust correspondence premise. It does not supply a generic +`KnownLayout` law, its `MaybeUninit` obligations, or pointer/reference safety. + +## Direct standard transmute + +`std-transmute` calls the standard primitive directly for `u32 -> [u8; 4]`, +the reverse conversion, and `u8 -> bool` after checking Boolean validity. It +rejects non-little-endian targets at compilation. Native emission succeeds and +retains all local wrapper bodies, with one external `core::intrinsics::transmute` +template. This observation isolates the standard primitive from the production +macro's unsupported compile-time helper loop; it does not prove the macro. + +The separate owned-operation proof adds an explicit interpretation-only provider +to that standard boundary. Admission checks each original Rust endpoint pair; +compiled auditing binds every reached call to its canonical provider and checks +the whole provider definitions. Numerical cases use the verified 64-bit +little-endian profile. The Boolean provider checks validity before construction +and returns forbidden execution outside its domain. Neither this boundary nor +its providers cover padding, pointers, resources, or arbitrary Rust types. diff --git a/verification/aeneas/tests/capabilities/invalid-transmute/Cargo.lock b/verification/aeneas/tests/capabilities/invalid-transmute/Cargo.lock new file mode 100644 index 0000000000..6b3392da3b --- /dev/null +++ b/verification/aeneas/tests/capabilities/invalid-transmute/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "owned-transmute-negative" +version = "0.0.0" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zerocopy" +version = "0.8.63" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/verification/aeneas/tests/capabilities/invalid-transmute/Cargo.toml b/verification/aeneas/tests/capabilities/invalid-transmute/Cargo.toml new file mode 100644 index 0000000000..99178873ae --- /dev/null +++ b/verification/aeneas/tests/capabilities/invalid-transmute/Cargo.toml @@ -0,0 +1,12 @@ +# Copyright 2026 The Fuchsia Authors +# SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +[package] +name = "owned-transmute-negative" +version = "0.0.0" +edition = "2021" + +[workspace] + +[dependencies] +zerocopy = { path = "../../../../../zerocopy", default-features = false } diff --git a/verification/aeneas/tests/capabilities/invalid-transmute/src/lib.rs b/verification/aeneas/tests/capabilities/invalid-transmute/src/lib.rs new file mode 100644 index 0000000000..2d6c73cb4d --- /dev/null +++ b/verification/aeneas/tests/capabilities/invalid-transmute/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright 2026 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. + +pub fn to_bool(value: u8) -> bool { + zerocopy::transmute!(value) +} diff --git a/verification/aeneas/tests/capabilities/known-layout/Cargo.lock b/verification/aeneas/tests/capabilities/known-layout/Cargo.lock new file mode 100644 index 0000000000..9499ba46fc --- /dev/null +++ b/verification/aeneas/tests/capabilities/known-layout/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "known-layout-probe" +version = "0.0.0" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zerocopy" +version = "0.8.63" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/verification/aeneas/tests/capabilities/known-layout/Cargo.toml b/verification/aeneas/tests/capabilities/known-layout/Cargo.toml new file mode 100644 index 0000000000..05ad38ca5b --- /dev/null +++ b/verification/aeneas/tests/capabilities/known-layout/Cargo.toml @@ -0,0 +1,12 @@ +# Copyright 2026 The Fuchsia Authors +# SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +[package] +name = "known-layout-probe" +version = "0.0.0" +edition = "2021" + +[workspace] + +[dependencies] +zerocopy = { path = "../../../../../zerocopy", default-features = false } diff --git a/verification/aeneas/tests/capabilities/known-layout/src/lib.rs b/verification/aeneas/tests/capabilities/known-layout/src/lib.rs new file mode 100644 index 0000000000..1661dab3bf --- /dev/null +++ b/verification/aeneas/tests/capabilities/known-layout/src/lib.rs @@ -0,0 +1,25 @@ +// Copyright 2026 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. + +use zerocopy::{DstLayout, KnownLayout}; + +pub fn layout>() -> DstLayout { + T::LAYOUT +} + +pub fn slice_u32_layout() -> DstLayout { + layout::<[u32]>() +} + +pub fn layout_unconstrained() -> DstLayout { + T::LAYOUT +} + +pub fn slice_u32_layout_direct() -> DstLayout { + <[u32] as KnownLayout>::LAYOUT +} diff --git a/verification/aeneas/tests/capabilities/owned-transmute/Cargo.lock b/verification/aeneas/tests/capabilities/owned-transmute/Cargo.lock new file mode 100644 index 0000000000..00b1b8beec --- /dev/null +++ b/verification/aeneas/tests/capabilities/owned-transmute/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "owned-transmute-probe" +version = "0.0.0" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zerocopy" +version = "0.8.63" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/verification/aeneas/tests/capabilities/owned-transmute/Cargo.toml b/verification/aeneas/tests/capabilities/owned-transmute/Cargo.toml new file mode 100644 index 0000000000..1a3d13ac7d --- /dev/null +++ b/verification/aeneas/tests/capabilities/owned-transmute/Cargo.toml @@ -0,0 +1,12 @@ +# Copyright 2026 The Fuchsia Authors +# SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +[package] +name = "owned-transmute-probe" +version = "0.0.0" +edition = "2021" + +[workspace] + +[dependencies] +zerocopy = { path = "../../../../../zerocopy", default-features = false } diff --git a/verification/aeneas/tests/capabilities/owned-transmute/src/lib.rs b/verification/aeneas/tests/capabilities/owned-transmute/src/lib.rs new file mode 100644 index 0000000000..6962e4eeda --- /dev/null +++ b/verification/aeneas/tests/capabilities/owned-transmute/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright 2026 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. + +pub fn to_bytes(value: u32) -> [u8; 4] { + zerocopy::transmute!(value) +} + +pub fn from_bytes(value: [u8; 4]) -> u32 { + zerocopy::transmute!(value) +} diff --git a/verification/aeneas/tests/capabilities/probe.py b/verification/aeneas/tests/capabilities/probe.py new file mode 100644 index 0000000000..6a997a5419 --- /dev/null +++ b/verification/aeneas/tests/capabilities/probe.py @@ -0,0 +1,97 @@ +# Copyright 2026 The Fuchsia Authors +# SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +"""Record exact native capability failures without modifying extracted ASTs.""" + +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import time + + +CASES = { + 'known-layout': 'known_layout_probe::layout,known_layout_probe::slice_u32_layout', + 'known-layout-unconstrained': 'known_layout_probe::layout_unconstrained', + 'known-layout-concrete': 'known_layout_probe::slice_u32_layout_direct', + 'owned-transmute': 'owned_transmute_probe::to_bytes,owned_transmute_probe::from_bytes', + 'invalid-transmute': 'owned_transmute_negative::to_bool', + 'std-transmute': 'std_transmute_probe::to_bytes,std_transmute_probe::from_bytes,std_transmute_probe::guarded_bool', + 'raw-pointers': 'raw_pointer_probe::cast,raw_pointer_probe::with_len,raw_pointer_probe::nonnull', +} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('case', choices=CASES) + parser.add_argument('--runtime', type=Path, required=True) + parser.add_argument('--cargo-home', type=Path, required=True) + parser.add_argument('--work', type=Path, required=True) + parser.add_argument('--cargo-target', type=Path, required=True) + args = parser.parse_args() + runtime, work = args.runtime.resolve(), args.work.resolve() + root = Path(__file__).resolve().parents[4] + sys.path.insert(0, str(root / 'verification/aeneas')) + import toolchain + metadata, charon_version = toolchain.validate(root, runtime) + fixture_case = ('known-layout' if args.case.startswith('known-layout') + else args.case) + fixture = Path(__file__).resolve().parent / fixture_case + work.mkdir(parents=True, exist_ok=False) + (work / 'native').mkdir(exist_ok=True) + environment = toolchain.runtime_env(runtime) + environment.update({ + 'RUSTUP_TOOLCHAIN': metadata['rust-toolchain-version'], + 'CARGO_HOME': str(args.cargo_home.resolve()), + 'CARGO_TARGET_DIR': str(args.cargo_target.resolve()), + 'CARGO_BUILD_JOBS': '2', 'CARGO_NET_OFFLINE': 'true', + 'CHARON_ADMISSION_SNAPSHOT': str(work / 'probe.before.ullbc'), + }) + commands = [ + ('charon', [str(runtime / 'charon'), 'cargo', '--preset=aeneas', + '--sysroot', 'default', '--start-from', CASES[args.case], + '--mir', 'promoted', '--no-dedup-serialized-ast', + '--dest-file', str(work / 'probe.llbc'), '--abort-on-error', + '--error-on-warnings', '--', '--lib', '--locked', '--offline']), + ('aeneas', [str(runtime / 'aeneas'), '-backend', 'lean', '-namespace', + 'Probe', '-dest', str(work / 'native'), '-split-files', + '-emit-json', '-abort-on-error', '-warnings-as-errors', + '-no-progress-bar', '-use-tuple-structs', 'false', + str(work / 'probe.llbc')]), + ] + sources = sorted((root / 'zerocopy/src').rglob('*.rs')) + [ + root / 'zerocopy/build.rs', root / 'zerocopy/Cargo.toml', + root / 'zerocopy/Cargo.lock', fixture / 'src/lib.rs', + fixture / 'Cargo.toml', fixture / 'Cargo.lock', Path(__file__).resolve(), + ] + evidence = {'case': args.case, 'fixture': str(fixture), + 'runtime': str(runtime), 'runtime_metadata': metadata, + 'charon_version': charon_version, + 'repository_head': subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], cwd=root, text=True).strip(), + 'repository_status': subprocess.check_output( + ['git', 'status', '--porcelain'], cwd=root, text=True), + 'source_sha256': {str(path.relative_to(root)): + hashlib.sha256(path.read_bytes()).hexdigest() + for path in sources}, 'steps': []} + for name, command in commands: + log_path = work / (name + '.log') + started = time.monotonic() + with log_path.open('w') as log: + result = subprocess.run(command, cwd=fixture, env=environment, + stdout=log, stderr=subprocess.STDOUT) + evidence['steps'].append({'name': name, 'command': command, + 'exit_code': result.returncode, + 'seconds': round(time.monotonic() - started, 2), + 'log': str(log_path)}) + (work / 'evidence.json').write_text(json.dumps(evidence, indent=2) + '\n') + print(name, result.returncode, log_path, flush=True) + if result.returncode: + return result.returncode + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/verification/aeneas/tests/capabilities/raw-pointers/Cargo.lock b/verification/aeneas/tests/capabilities/raw-pointers/Cargo.lock new file mode 100644 index 0000000000..a4c65c14e6 --- /dev/null +++ b/verification/aeneas/tests/capabilities/raw-pointers/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-pointer-probe" +version = "0.0.0" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zerocopy" +version = "0.8.63" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/verification/aeneas/tests/capabilities/raw-pointers/Cargo.toml b/verification/aeneas/tests/capabilities/raw-pointers/Cargo.toml new file mode 100644 index 0000000000..630530baec --- /dev/null +++ b/verification/aeneas/tests/capabilities/raw-pointers/Cargo.toml @@ -0,0 +1,12 @@ +# Copyright 2026 The Fuchsia Authors +# SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +[package] +name = "raw-pointer-probe" +version = "0.0.0" +edition = "2021" + +[workspace] + +[dependencies] +zerocopy = { path = "../../../../../zerocopy", default-features = false } diff --git a/verification/aeneas/tests/capabilities/raw-pointers/src/lib.rs b/verification/aeneas/tests/capabilities/raw-pointers/src/lib.rs new file mode 100644 index 0000000000..8244c2fff0 --- /dev/null +++ b/verification/aeneas/tests/capabilities/raw-pointers/src/lib.rs @@ -0,0 +1,21 @@ +// Copyright 2026 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. + +use core::ptr::NonNull; + +pub fn cast(pointer: *const u32) -> *const u8 { + pointer as *const u8 +} + +pub fn with_len(pointer: *const u8, count: usize) -> *const [u8] { + core::ptr::slice_from_raw_parts(pointer, count) +} + +pub fn nonnull(pointer: *mut u8) -> Option> { + NonNull::new(pointer) +} diff --git a/verification/aeneas/tests/capabilities/std-transmute/Cargo.lock b/verification/aeneas/tests/capabilities/std-transmute/Cargo.lock new file mode 100644 index 0000000000..81c95e1f23 --- /dev/null +++ b/verification/aeneas/tests/capabilities/std-transmute/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "std-transmute-probe" +version = "0.0.0" diff --git a/verification/aeneas/tests/capabilities/std-transmute/Cargo.toml b/verification/aeneas/tests/capabilities/std-transmute/Cargo.toml new file mode 100644 index 0000000000..5dfbe5f385 --- /dev/null +++ b/verification/aeneas/tests/capabilities/std-transmute/Cargo.toml @@ -0,0 +1,9 @@ +# Copyright 2026 The Fuchsia Authors +# SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +[package] +name = "std-transmute-probe" +version = "0.0.0" +edition = "2021" + +[workspace] diff --git a/verification/aeneas/tests/capabilities/std-transmute/src/lib.rs b/verification/aeneas/tests/capabilities/std-transmute/src/lib.rs new file mode 100644 index 0000000000..73ca76f63e --- /dev/null +++ b/verification/aeneas/tests/capabilities/std-transmute/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2026 The Fuchsia Authors +// SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT + +#[cfg(not(target_endian = "little"))] +compile_error!("this concrete byte-conversion probe supports little-endian targets only"); + +#[allow(unnecessary_transmutes)] +pub fn to_bytes(value: u32) -> [u8; 4] { + unsafe { core::mem::transmute::(value) } +} + +#[allow(unnecessary_transmutes)] +pub fn from_bytes(value: [u8; 4]) -> u32 { + unsafe { core::mem::transmute::<[u8; 4], u32>(value) } +} + +pub fn guarded_bool(value: u8) -> bool { + assert!(value < 2); + unsafe { core::mem::transmute::(value) } +} diff --git a/zerocopy/src/layout/mod.rs b/zerocopy/src/layout/mod.rs index 1a5c8cb965..f2ded8d82e 100644 --- a/zerocopy/src/layout/mod.rs +++ b/zerocopy/src/layout/mod.rs @@ -1693,6 +1693,11 @@ impl DstLayout { _ => None, } }))] + /// + /// ```aeneas + /// spec metadata_exact_spec + /// ensures(raw) r => metadataSpec self size.val r + /// ``` const fn metadata_for_exact_size(&self, size: usize) -> Option { #[cfg(all(kani, kani_slow))] #[kani::proof_for_contract(DstLayout::metadata_for_exact_size)] @@ -1822,6 +1827,15 @@ impl DstLayout { _ => false, } }))] + /// + /// ```aeneas + /// spec validate_cast_spec + /// requires(raw) hroom : addr.val + bytes_len.val ≤ Usize.max + /// requires(raw) ht : match self.size_info with + /// | .Sized _ => True + /// | .SliceDst tail => 0 < tail.elem_size.val + /// ensures(raw) r => castSpec self addr.val bytes_len.val cast_type r + /// ``` pub(crate) const fn validate_cast_and_convert_metadata( &self, addr: usize, diff --git a/zerocopy/src/layout/primitive_checks.rs b/zerocopy/src/layout/primitive_checks.rs index b1b0b2041f..bd34705c33 100644 --- a/zerocopy/src/layout/primitive_checks.rs +++ b/zerocopy/src/layout/primitive_checks.rs @@ -80,6 +80,45 @@ fn check_empty_layout(repr_align: Option) { assert!(empty.statically_shallow_unpadded); } +/// Checks the actual built-in `[u32]` descriptor and every element count. +/// +/// The ABI guard makes the primitive-read premises visible in the extracted +/// model. The comparison retains overflow: both computations must return +/// `None` when the mathematical byte length does not fit in `usize`. +/// This checks one associated constant, not the complete `KnownLayout` trait. +/// +/// ```aeneas +/// spec u32_slice_known_layout_checks_spec +/// ensures _ => True +/// ``` +#[allow(clippy::arithmetic_side_effects)] +fn check_u32_slice_known_layout(elems: usize) { + let size = mem::size_of::(); + let align = mem::align_of::(); + if !align.is_power_of_two() { + return; + } + if size % align != 0 { + return; + } + + let slice = <[u32] as crate::KnownLayout>::LAYOUT; + assert!(slice.align.get() == align); + assert!(slice.statically_shallow_unpadded); + match slice.size_info { + SizeInfo::SliceDst(tail) => { + assert!(tail.offset == 0); + assert!(tail.elem_size == size); + assert!(tail.size_base == 0); + assert!(tail.size_rounding_align_and_phase.0.get() == align); + let actual = tail.size_for_elems(elems); + let expected = elems.checked_mul(size); + assert!(super::tail_checks::same_optional_usize(actual, expected)); + } + SizeInfo::Sized { .. } => panic!("slice descriptor returned a sized layout"), + } +} + #[cfg(test)] mod tests { use super::*; @@ -92,6 +131,9 @@ mod tests { check_primitive_layouts::<(u8, u64)>(); check_primitive_layouts::<[u64; 3]>(); check_empty_layout(None); + for elems in [0, 1, usize::MAX / 4, usize::MAX / 4 + 1, usize::MAX] { + check_u32_slice_known_layout(elems); + } for align in [1, 2, 8, 3] { check_empty_layout(NonZeroUsize::new(align)); } diff --git a/zerocopy/src/layout/tail_checks.rs b/zerocopy/src/layout/tail_checks.rs index 2a0e616841..bd31bbfcf8 100644 --- a/zerocopy/src/layout/tail_checks.rs +++ b/zerocopy/src/layout/tail_checks.rs @@ -24,7 +24,7 @@ use core::num::NonZeroUsize; -use super::TrailingSliceLayout; +use super::{CastType, DstLayout, MetadataCastError, SizeInfo, TrailingSliceLayout}; pub(crate) fn same_optional_usize(left: Option, right: Option) -> bool { match (left, right) { @@ -75,6 +75,84 @@ fn reference_capacity( } } +fn reference_metadata( + runtime_layout: DstLayout, + align: NonZeroUsize, + phase: usize, + size: usize, +) -> Option { + match runtime_layout.size_info { + SizeInfo::Sized { .. } => None, + SizeInfo::SliceDst(tail) => { + if tail.elem_size == 0 { + return None; + } + match reference_capacity(tail, align, phase, size) { + None => None, + Some(bytes) => { + #[allow(clippy::arithmetic_side_effects)] + let elems = bytes / tail.elem_size; + if same_optional_usize(reference_size(tail, align, phase, elems), Some(size)) { + Some(elems) + } else { + None + } + } + } + } + } +} + +#[allow(clippy::arithmetic_side_effects)] +fn reference_cast( + runtime_layout: DstLayout, + align: NonZeroUsize, + phase: usize, + addr: usize, + length: usize, + side: CastType, +) -> Result<(usize, usize), MetadataCastError> { + // The caller checks address addition and excludes the zero-stride panic + // before either the production or reference cast is evaluated. + let anchor = match side { + CastType::Prefix => addr, + CastType::Suffix => addr + length, + }; + // Alignment failure has priority over insufficient size. + if anchor % runtime_layout.align.get() != 0 { + return Err(MetadataCastError::Alignment); + } + let candidate = match runtime_layout.size_info { + SizeInfo::Sized { size } => { + if size <= length { + Some((0, size)) + } else { + None + } + } + SizeInfo::SliceDst(tail) => match reference_capacity(tail, align, phase, length) { + None => None, + Some(bytes) => { + let elems = bytes / tail.elem_size; + match reference_size(tail, align, phase, elems) { + Some(size) => Some((elems, size)), + None => None, + } + } + }, + }; + match candidate { + None => Err(MetadataCastError::Size), + Some((elems, size)) => { + let split = match side { + CastType::Prefix => size, + CastType::Suffix => length - size, + }; + Ok((elems, split)) + } + } +} + /// Check the entire optional size and capacity results, including overflow. /// /// Physical slice offset does not constrain this claim: a raw layout can have @@ -109,3 +187,210 @@ fn trailing_arithmetic_check( )); } } + +/// Check exact-size metadata and complete cast results against independent +/// calculations. A successful cast must select the greatest fitting element +/// count and its padded prefix/suffix split; errors must have the same kind. +/// +/// The address guard is the production method's no-overflow premise. The +/// explicit zero-stride guard excludes its documented panic, even when an +/// alignment error would otherwise be returned. Exact-size inference itself +/// is also checked for zero-stride tails, for which it must return `None`. +/// +/// ```aeneas +/// spec layout_observations_check_spec +/// ensures(raw) _ => True +/// ``` +fn layout_observations_check( + runtime_layout: DstLayout, + align: NonZeroUsize, + phase: usize, + size: usize, + addr: usize, + length: usize, + side: CastType, +) { + let encoding_matches = match runtime_layout.size_info { + SizeInfo::Sized { .. } => true, + SizeInfo::SliceDst(tail) => { + align.get().is_power_of_two() + && phase < align.get() + && same_optional_usize( + align.get().checked_add(phase), + Some(tail.size_rounding_align_and_phase.0.get()), + ) + } + }; + if encoding_matches { + assert!(same_optional_usize( + runtime_layout.metadata_for_exact_size(size), + reference_metadata(runtime_layout, align, phase, size), + )); + let nonzero_stride = match runtime_layout.size_info { + SizeInfo::Sized { .. } => true, + SizeInfo::SliceDst(tail) => tail.elem_size != 0, + }; + if addr.checked_add(length).is_some() && nonzero_stride { + let actual = runtime_layout.validate_cast_and_convert_metadata(addr, length, side); + let expected = reference_cast(runtime_layout, align, phase, addr, length, side); + let same = match (actual, expected) { + (Ok((elems, split)), Ok((expected_elems, expected_split))) => { + elems == expected_elems && split == expected_split + } + (Err(MetadataCastError::Alignment), Err(MetadataCastError::Alignment)) + | (Err(MetadataCastError::Size), Err(MetadataCastError::Size)) => true, + _ => false, + }; + assert!(same); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::layout::RoundingAlignAndPhase; + + fn tail_layout( + outer_align: usize, + size_align: usize, + elem_size: usize, + size_base: usize, + phase: usize, + offset: usize, + ) -> DstLayout { + DstLayout { + align: NonZeroUsize::new(outer_align).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(size_align).unwrap(), + phase, + ), + }), + statically_shallow_unpadded: false, + } + } + + fn check_cast( + layout: DstLayout, + size_align: usize, + phase: usize, + addr: usize, + length: usize, + side: CastType, + expected: Result<(usize, usize), MetadataCastError>, + ) { + let size_align = NonZeroUsize::new(size_align).unwrap(); + // The proved root compares complete results, including both error + // kinds. Pin the reference result too, so each case documents its path. + layout_observations_check(layout, size_align, phase, length, addr, length, side); + assert!(match (reference_cast(layout, size_align, phase, addr, length, side), expected) { + (Ok(actual), Ok(expected)) => actual == expected, + (Err(MetadataCastError::Alignment), Err(MetadataCastError::Alignment)) + | (Err(MetadataCastError::Size), Err(MetadataCastError::Size)) => true, + _ => false, + }); + } + + fn check_both( + layout: DstLayout, + size_align: usize, + phase: usize, + length: usize, + elems: usize, + size: usize, + ) { + check_cast(layout, size_align, phase, 0, length, CastType::Prefix, Ok((elems, size))); + check_cast( + layout, + size_align, + phase, + 0, + length, + CastType::Suffix, + Ok((elems, length.checked_sub(size).unwrap())), + ); + } + + #[test] + fn cast_rounded_size_branches() { + // An input below the rounding alignment can select an empty object. + check_both(tail_layout(1, 8, 1, 0, 0, 4), 8, 0, 7, 0, 0); + + // Strides below, equal to, and above the size alignment all use the + // complete size. Nonzero base and phase differ from physical offset. + // For stride 10, the unused remainders are 2, 4, and 6: below, equal + // to, and above alignment 4. Only the last two discard a rounded unit. + for &(align, stride, length, elems, size) in &[ + (8, 3, 29, 7, 29), + (4, 4, 29, 5, 29), + (4, 10, 29, 2, 29), + (4, 10, 21, 1, 17), + (4, 10, 33, 2, 29), + ] { + check_both(tail_layout(1, align, stride, 5, 2, 63), align, 2, length, elems, size); + } + + // This raw layout need not describe a completed Rust type. Comparing + // stride 6 to outer alignment 8 would incorrectly retain all 16 bytes; + // size alignment 4 requires discarding the aligned remainder of 4. + check_both(tail_layout(8, 4, 6, 0, 0, 3), 4, 0, 16, 2, 12); + } + + #[test] + fn cast_errors_keep_alignment_priority() { + let sized = DstLayout { + align: NonZeroUsize::new(8).unwrap(), + size_info: SizeInfo::Sized { size: 24 }, + statically_shallow_unpadded: false, + }; + check_both(sized, 4, 2, 32, 0, 24); + for &layout in &[sized, tail_layout(8, 4, 6, 16, 2, 3)] { + for &side in &[CastType::Prefix, CastType::Suffix] { + check_cast(layout, 4, 2, 1, 4, side, Err(MetadataCastError::Alignment)); + let addr = match side { + CastType::Prefix => 0, + CastType::Suffix => 4, + }; + check_cast(layout, 4, 2, addr, 4, side, Err(MetadataCastError::Size)); + } + } + } + + #[test] + fn cast_sizes_near_word_limit() { + let max = usize::MAX; + let rounded = max.checked_sub(7).unwrap(); + check_both(tail_layout(1, 8, 1, 0, 0, max), 8, 0, max, rounded, rounded); + check_both(tail_layout(1, 4, 8, 0, 0, max), 4, 0, max, rounded / 8, rounded); + check_both(tail_layout(1, 4, 1, rounded, 3, 0), 4, 3, max, 1, max.checked_sub(3).unwrap()); + let sized = DstLayout { + align: NonZeroUsize::new(1).unwrap(), + size_info: SizeInfo::Sized { size: max }, + statically_shallow_unpadded: false, + }; + check_both(sized, 4, 2, max, 0, max); + let overflowing = tail_layout(1, 4, 1, max.checked_sub(3).unwrap(), 1, 0); + for &side in &[CastType::Prefix, CastType::Suffix] { + check_cast(overflowing, 4, 1, 0, max, side, Err(MetadataCastError::Size)); + check_cast( + tail_layout(1, 4, 6, 0, 0, 3), + 4, + 0, + max.checked_sub(16).unwrap(), + 16, + side, + Ok(( + 2, + match side { + CastType::Prefix => 12, + CastType::Suffix => 4, + }, + )), + ); + } + } +}