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, + }, + )), + ); + } + } +}