Skip to content
hopper
Get started
Safety / unsafe

Unsafe invariants

The audit ledger for raw-pointer boundaries and safety contracts.

This reference covers Hopper 0.5.0, published on crates.io. See the release status for package verification, dated benchmarks, and remaining audit work.

Trust Posture

Hopper is a safety-layered framework that deliberately uses unsafe for the operations where Rust's ownership system cannot express the invariants we need (pointer casts onto account byte slices, zero-copy overlays, CPI invocation). Every other layer of the framework (header validation, fingerprint checking, tiered loading, frame borrow tracking, validation graphs) exists to make the unsafe core as small and auditable as possible.

Design commitment: unsafe is never used for convenience. It is used only when a safe alternative would require allocation, serialization, or loss of direct, allocation-free account access.

Audit scope: the unsafe surface audited by this ledger spans the raw source directories that own Hopper's zero-copy and backend boundary code: crates/hopper-native/src, crates/hopper-runtime/src, crates/hopper-solana/src, and crates/hopper-core/src. Unsafe in those directories is limited to raw Solana/backend ABI crossings, zero-copy overlays, Pod reads/writes, CPI assembly, and bounded in-place collection operations.

This ledger explains the major unsafe boundaries, their justifications, and the invariants that must hold. The generated CI inventory, not the manually maintained table below, is the exhaustive file/count record for a particular source revision.

CI also emits a machine-generated unsafe inventory artifact. The workflow .github/workflows/unsafe-safety.yml runs scripts/check-unsafe-safety-comments.py --inventory-out unsafe-inventory.md and uploads the resulting markdown table as the unsafe-inventory artifact for each push and pull request.

Source-pinned unsafe inventory

This generated snapshot replaces the historical table, including removed compatibility modules. Counts identify review surfaces, not a proof of soundness. Regenerate for a new executable-source revision. The scan covers all 29 public packages; the detailed explanations below focus on the account/runtime layers.

Generated by scripts/check-unsafe-safety-comments.py.

Publish-order SHA-256: b1c40b99a433651392be13f15d49c4633caf49911e56e89df3adc45b0fb70a20 Source commit: f642abad62034fd688c493991c9d123e05f03388 Source tree clean: true Public packages: 29 Checked Rust files: 310

Source file Blocks Unsafe fns Public unsafe fns Unsafe traits Unsafe impls Unsafe extern blocks
crates/hopper-builtins/src/lib.rs 27 8 8 0 0 0
crates/hopper-core/src/abi/boolean.rs 0 0 0 0 4 0
crates/hopper-core/src/abi/field_ref.rs 1 0 0 0 0 0
crates/hopper-core/src/abi/integers.rs 0 0 0 0 4 0
crates/hopper-core/src/abi/mod.rs 0 0 0 1 0 0
crates/hopper-core/src/abi/typed_address.rs 2 0 0 0 6 0
crates/hopper-core/src/account/cursor.rs 1 0 0 0 0 0
crates/hopper-core/src/account/header.rs 0 0 0 0 3 0
crates/hopper-core/src/account/lifecycle.rs 3 0 0 0 0 0
crates/hopper-core/src/account/pod.rs 6 2 2 0 0 0
crates/hopper-core/src/account/reader.rs 2 0 0 0 0 0
crates/hopper-core/src/account/registry.rs 8 0 0 0 2 0
crates/hopper-core/src/account/segment.rs 6 0 0 0 3 0
crates/hopper-core/src/account/verified.rs 6 0 0 0 0 0
crates/hopper-core/src/accounts/hopper_account.rs 1 1 1 0 0 0
crates/hopper-core/src/accounts/program_account.rs 1 1 1 0 0 0
crates/hopper-core/src/accounts/segmented.rs 1 0 0 0 0 0
crates/hopper-core/src/accounts/unchecked.rs 1 1 1 0 0 0
crates/hopper-core/src/check/fast.rs 3 1 0 0 0 0
crates/hopper-core/src/check/guards.rs 1 0 0 0 0 0
crates/hopper-core/src/check/mod.rs 4 0 0 0 0 0
crates/hopper-core/src/collections/fixed_vec.rs 6 0 0 0 0 0
crates/hopper-core/src/collections/journal.rs 3 0 0 0 0 0
crates/hopper-core/src/collections/packed_map.rs 4 0 0 0 2 0
crates/hopper-core/src/collections/ring_buffer.rs 2 0 0 0 0 0
crates/hopper-core/src/collections/slab.rs 5 0 0 0 2 0
crates/hopper-core/src/collections/slot_map.rs 3 0 0 0 0 0
crates/hopper-core/src/collections/sorted_vec.rs 5 0 0 0 0 0
crates/hopper-core/src/cpi/mod.rs 8 0 0 0 0 0
crates/hopper-core/src/event/mod.rs 3 0 0 0 0 0
crates/hopper-core/src/frame/mod.rs 17 1 1 0 2 0
crates/hopper-core/src/frame/phase.rs 6 0 0 0 0 0
crates/hopper-core/src/manifest.rs 4 0 0 0 4 0
crates/hopper-core/src/virtual_state/mod.rs 5 0 0 0 0 0
crates/hopper-macros/src/lib.rs 2 1 1 0 6 0
crates/hopper-macros-proc/src/args.rs 1 0 0 0 0 0
crates/hopper-macros-proc/src/context.rs 7 1 0 0 0 0
crates/hopper-macros-proc/src/event.rs 1 0 0 0 0 0
crates/hopper-macros-proc/src/pod.rs 0 0 0 0 3 0
crates/hopper-macros-proc/src/state.rs 0 0 0 0 6 0
crates/hopper-memo/src/lib.rs 1 0 0 0 0 0
crates/hopper-native/src/account_view.rs 52 12 12 0 2 0
crates/hopper-native/src/address.rs 1 0 0 0 0 0
crates/hopper-native/src/borrow.rs 5 2 2 0 0 0
crates/hopper-native/src/budget.rs 2 0 0 0 0 0
crates/hopper-native/src/cpi.rs 15 2 2 0 0 0
crates/hopper-native/src/entrypoint.rs 20 8 4 0 2 0
crates/hopper-native/src/error.rs 1 0 0 0 0 0
crates/hopper-native/src/hash.rs 3 0 0 0 0 0
crates/hopper-native/src/instruction.rs 8 0 0 0 0 0
crates/hopper-native/src/introspect.rs 2 0 0 0 0 0
crates/hopper-native/src/lazy.rs 27 2 1 0 2 0
crates/hopper-native/src/lens.rs 13 0 0 0 0 0
crates/hopper-native/src/log.rs 5 0 0 0 0 0
crates/hopper-native/src/mem.rs 13 4 4 0 0 0
crates/hopper-native/src/pda.rs 19 0 0 0 0 0
crates/hopper-native/src/pod.rs 1 0 0 3 27 0
crates/hopper-native/src/project.rs 10 3 3 2 13 0
crates/hopper-native/src/raw_input.rs 99 7 7 0 0 0
crates/hopper-native/src/return_data.rs 3 0 0 0 0 0
crates/hopper-native/src/syscalls.rs 1 2 0 0 0 0
crates/hopper-native/src/system.rs 2 0 0 0 0 0
crates/hopper-native/src/sysvar.rs 9 0 0 0 0 0
crates/hopper-native/src/verify.rs 1 0 0 0 0 0
crates/hopper-native/src/wire.rs 0 0 0 0 12 0
crates/hopper-runtime/src/account.rs 42 9 8 0 16 0
crates/hopper-runtime/src/account_wrappers.rs 3 7 7 0 6 0
crates/hopper-runtime/src/address.rs 3 0 0 0 3 0
crates/hopper-runtime/src/audit.rs 2 0 0 0 0 0
crates/hopper-runtime/src/behavior.rs 2 0 0 0 2 0
crates/hopper-runtime/src/borrow.rs 14 2 2 0 0 0
crates/hopper-runtime/src/borrow_registry.rs 1 0 0 0 1 0
crates/hopper-runtime/src/compact.rs 0 0 0 0 2 0
crates/hopper-runtime/src/context.rs 12 4 4 0 2 0
crates/hopper-runtime/src/cpi.rs 17 2 2 0 0 0
crates/hopper-runtime/src/cpi_dedup_tests.rs 2 0 0 0 0 0
crates/hopper-runtime/src/cpi_event.rs 4 0 0 0 0 0
crates/hopper-runtime/src/crypto.rs 12 0 0 0 0 0
crates/hopper-runtime/src/dyn_cpi.rs 9 0 0 0 0 0
crates/hopper-runtime/src/error.rs 3 0 0 0 0 0
crates/hopper-runtime/src/foreign.rs 4 1 1 0 0 0
crates/hopper-runtime/src/instruction.rs 5 0 0 0 0 0
crates/hopper-runtime/src/interop.rs 2 0 0 1 2 0
crates/hopper-runtime/src/lamports.rs 2 0 0 0 0 0
crates/hopper-runtime/src/layout.rs 3 0 0 0 0 0
crates/hopper-runtime/src/lazy.rs 8 0 0 0 0 0
crates/hopper-runtime/src/lib.rs 18 3 3 0 0 0
crates/hopper-runtime/src/log.rs 2 0 0 0 0 0
crates/hopper-runtime/src/memory.rs 8 4 4 0 0 0
crates/hopper-runtime/src/migrate.rs 8 0 0 0 21 0
crates/hopper-runtime/src/native_boundary.rs 9 5 5 0 0 0
crates/hopper-runtime/src/option_byte.rs 2 0 0 0 0 0
crates/hopper-runtime/src/pda.rs 2 0 0 0 0 0
crates/hopper-runtime/src/remaining.rs 2 0 0 0 0 0
crates/hopper-runtime/src/return_data.rs 4 0 0 0 0 0
crates/hopper-runtime/src/segment_borrow.rs 16 1 1 0 4 0
crates/hopper-runtime/src/segment_lease.rs 5 2 2 0 0 0
crates/hopper-runtime/src/syscall.rs 1 0 0 0 0 0
crates/hopper-runtime/src/syscalls.rs 25 19 19 0 0 0
crates/hopper-runtime/src/tail.rs 1 0 0 0 0 0
crates/hopper-runtime/src/token.rs 15 0 0 0 0 0
crates/hopper-runtime/src/write_policy.rs 5 0 0 0 1 0
crates/hopper-runtime/src/zerocopy.rs 0 0 0 2 5 0
crates/hopper-sdk/src/reader.rs 0 1 1 0 0 0
crates/hopper-solana/src/compute.rs 1 0 0 0 0 0
crates/hopper-solana/src/crypto/merkle.rs 2 0 0 0 0 0
crates/hopper-solana/src/mint.rs 2 0 0 0 0 0
crates/hopper-solana/src/token.rs 2 0 0 0 0 0
crates/hopper-svm/src/lib.rs 4 0 0 0 0 0
src/__macro_support.rs 1 0 0 0 0 0
src/receipts.rs 2 0 0 0 0 0

Trust Summary

Hopper's unsafe surface is deliberately narrow and follows three foundational rules:

  1. All overlay targets are alignment-1, enforced by the type system. The Pod marker is implemented only for alignment-1 types (u8, i8, [u8; N], and macro-authored wire/layout types), so every reference-returning overlay API (load, segment_ref, raw_ref, pod_from_bytes, …) bounds on Pod and cannot be instantiated with a native multi-byte integer, segment_ref::<u64> is a compile error (see tests/hopper-trybuild/tests/ui/fail/*_u64_rejected.rs). Native scalars are read by value via the separate ValuePod marker and read_unaligned/Context::read_data, never as &u64. The two internal header reads that previously used aligned casts (AccountView::header_u32, the fast-entrypoint length prefix) now use core::ptr::read_unaligned. No pointer cast in the codebase produces a reference with align > 1; this eliminates alignment UB entirely.
  2. All casts are bounds-checked. Every pod_from_bytes / overlay_at / read_unaligned call is preceded by a length check against T::SIZE or explicit offset arithmetic.
  3. Aliasing is structurally prevented. Mutable borrows flow through &mut self (compile-time) or frame-level bitmask tracking (runtime). No two mutable references can alias the same account data.

What tests prove it

Invariant Focused suite Command
Pod boundary rejection tests/unsafe_boundary_tests.rs cargo test --test unsafe_boundary_tests
Overlay checked/unchecked parity tests/overlay_equivalence_tests.rs cargo test --test overlay_equivalence_tests
Compat regression and receipt wire format tests/compat_regression_tests.rs cargo test --test compat_regression_tests
Property-based ABI roundtrip tests/property_tests.rs cargo test --test property_tests
CPI guard, collections, registry, validation tests/trust_tests.rs cargo test --test trust_tests

The suites are the durable evidence; their case counts move as regressions are added and are intentionally not frozen here.

What callers must guarantee

API Caller Obligation
unsafe impl Pod for T T is #[repr(C)] or #[repr(transparent)], all fields are [u8; N] or Pod, align_of::<T>() == 1
cast_unchecked / cast_unchecked_mut data.len() >= size_of::<T>(). No concurrent aliasing.
hopper_layout! load_unchecked Account data is valid for the layout. Caller accepts all risk.
MaybeUninit transmute in CPI builders All ACCTS slots initialized via add_account() before invoke()

Global Guarantees

  1. #![deny(unsafe_op_in_unsafe_fn)] -- enforced in hopper-core and hopper-solana. All unsafe operations must be explicitly wrapped even inside unsafe fn.
  2. Pod trait -- unsafe trait Pod: Copy + Sized requires align_of == 1 and all bit patterns valid. Every unsafe impl Pod is for types whose fields are [u8; N] or nested Pod types under #[repr(C)]/#[repr(transparent)].
  3. All pointer casts target align-1 types. No pointer cast in the codebase produces a reference to a type with alignment > 1.

hopper-core::abi

Wire types (integers.rs, boolean.rs)

Line(s) Construct Invariant
unsafe impl WireType Trait impl per wire type Type is #[repr(transparent)] over [u8; N], align == 1, size == N (compile-time asserted). All bit patterns valid.
unsafe impl Pod Trait impl per wire type Same as above.

typed_address.rs

Line Construct Invariant
61 unsafe impl Pod for TypedAddress<T> #[repr(transparent)] over [u8; 32]. PhantomData is ZST. Size == 32, align == 1 (compile-time asserted).
99 &*(account.address() as *const Address as *const [u8; 32]) hopper_native::Address is [u8; 32] (same repr). Read-only, no-alloc.
198 unsafe impl Pod for UntypedAddress #[repr(transparent)] over [u8; 32].

field_ref.rs

Line Construct Invariant
88 &*(self.data.as_ptr() as *const [u8; 32]) Slice length checked ≥ 32 before cast. Target type is [u8; 32], align 1.

hopper-core::account

pod.rs

Line Construct Invariant
13 pub unsafe trait Pod Marker trait. Implementors guarantee align-1, all bit patterns valid.
16-17 unsafe impl Pod for u8 / [u8; 32] Trivially safe.
39 pod_from_bytes: &*(data.as_ptr() as *const T) Size checked: data.len() >= T::SIZE. T: Pod guarantees align-1. No aliasing: immutable borrow.
54 pod_from_bytes_mut: &mut *(data.as_mut_ptr() as *mut T) Size checked. T: Pod. Caller must ensure exclusive access.
64 pod_read: read_unaligned Size checked. T: Pod. Returns by value, no alias concern.
74 pod_write: write_unaligned Size checked. T: Pod. Caller must hold &mut [u8].

header.rs

Line Construct Invariant
49 unsafe impl Pod for AccountHeader #[repr(C)] of all byte-array fields. Size == 16, align == 1 (asserted).

verified.rs

All checked core Pod casts and verified overlay constructors enforce FixedLayout::SIZE == size_of::<T>() using a framework-owned const assertion. The compatibility _SIZE_IS_HONEST associated constant is overridable in safe Rust and is not trusted by internal pointer arithmetic. Collections check the same equality independently and additionally reject zero-sized elements. Segment slices validate count against capacity, descriptor element size against the type, and checked region bounds before exposing elements.

Runtime AccountView typed loads independently bound offset + size_of::<T>() with checked arithmetic. Safe custom layout implementations can override both validation and size methods, so neither is relied upon for pointer safety. Compact initialization checks the real head range before stamping the discriminator, and rejects overlapping or overflowing tail-prefix offsets.

Line Construct Invariant
36 VerifiedAccount::get(): &*(data.as_ptr() as *const T) Size validated at construction (data.len() >= T::SIZE). T: Pod. Immutable.
99 overlay_at: &*(data.as_ptr().add(offset) as *const U) Bounds checked: offset + U::SIZE <= data.len(). U: Pod.
126 VerifiedAccountMut::get() Same as VerifiedAccount::get().
133 get_mut(): &mut *(data.as_mut_ptr() as *mut T) Size validated. Exclusive access via &mut self.
180 overlay_at (mut variant) Bounds checked.
190 overlay_at_mut Bounds checked. Exclusive access via &mut self.

VerifiedAccount and VerifiedAccountMut are size-checked overlay wrappers. Their public constructors establish sufficient byte length, not account ownership, header validity, signer authority, PDA derivation, or write policy. Those checks belong to the loader or caller. They may return &T / &mut T, &[u8], &mut [u8], or secondary overlays, but every returned reference is tied to &self or &mut self. The wrapper itself owns either the Hopper borrow guard or a size-checked raw slice, so the reference cannot outlive the wrapper and cannot be held after the guard is dropped. This is intentionally distinct from the HopperRefOnly field-access path, where naked raw references are rejected at the macro boundary.

The compile-fail fixture tests/hopper-trybuild/tests/ui/fail/verified_ref_outlives_wrapper.rs locks in this lifetime boundary: attempting to return a VerifiedAccount::get() result after the wrapper goes out of scope fails to compile.

reader.rs

Line Construct Invariant
42 Header overlay cast Data length checked ≥ HEADER_LEN at construction.
114 Address overlay at offset Bounds checked.

segment.rs

Line Construct Invariant
37 unsafe impl Pod for SegmentDescriptor #[repr(C)], all byte fields.
147, 175, 246, 257, 310, 321 Pointer offset casts All bounds-checked before cast. Target types are Pod (align-1).

registry.rs

Line Construct Invariant
226, 236, 268, 320, 418, 439, 449, 489 Pointer offset casts All preceded by bounds checks against self.data.len(). Target types: SegmentEntry (Pod, align-1), or generic T: Pod.

cursor.rs

Line Construct Invariant
135 Address cast at cursor position Position + 32 <= data.len() checked.

lifecycle.rs

No raw unsafe blocks. Uses hopper_runtime::AccountView safe APIs.


hopper-core::check

mod.rs

Line Construct Invariant
142 keys_eq_fast: read_unaligned x 4 Input is &[u8; 32], always valid for u64 reads at offsets 0/8/16/24.
159 is_zero_address: read_unaligned x 4 Same as above.
179 Address cast in check_has_one hopper_native::Address is [u8; 32].
239 borrow_unchecked() in check_account Immutable borrow for validation only. No conflicting mutable borrows at this point (called before execution phase).
353 borrow_unchecked() in check_discriminator (via macro) Same pattern.
405 account.owner() in check_owner_multi AccountView's unsafe owner() reads the owner field. No alias concern (read-only).

fast.rs

Line Construct Invariant
71-82 read_account_header Reads first 4 bytes of RuntimeAccount via pointer dereference. Relies on AccountView being #[repr(C)] with first field = pointer to RuntimeAccount base. Gated to target_os = "solana" only. Preconditions: SVM guarantees valid input buffer layout.
103 Call to read_account_header Within check_account_fast, called on SVM-provided AccountView.

modifier.rs

Line Construct Invariant
160 borrow_unchecked() in Account<T>::from_account Owner check passed. Frame-level borrow tracking prevents conflicting mutable borrows.
179 borrow_unchecked_mut() in AccountMut<T>::from_account Owner + writable checks passed. Caller ensures exclusive access at frame level.

hopper-core::cpi

mod.rs

Line Construct Invariant
58, 207 MaybeUninit::uninit().assume_init() (array of MaybeUninit) Creating an array of MaybeUninit<&AccountView> from uninit is sound: MaybeUninit<T> does not require initialization. Added slots are initialized via add_account before invoke.
76, 224 Address cast from AccountView::address() Address is [u8; 32]. Read-only cast.
122, 260 View transmute from MaybeUninit array All ACCTS slots initialized via add_account (enforced by debug_assert_eq!(acct_cursor, ACCTS)). The transmute from [MaybeUninit<&T>; N] to [&T; N] is sound when all N elements are initialized.
128, 265 core::mem::zeroed() for InstructionAccount array InstructionAccount has no invalid bit patterns (contains &[u8; 32] pointer + 2 bools). Zeroed pointers are overwritten before use.
150, 154, 285, 287 core::mem::zeroed() for Signer/Seed buffers Same pattern. All used slots are written before invoke_signed_unchecked.

hopper-core::collections

All collections follow the same pattern: bounds-checked pointer arithmetic on &[u8] / &mut [u8] slices, with target types that are Pod (align-1).

Module Pattern Invariant
fixed_vec read_unaligned, overlay casts Count/capacity validated. Offset arithmetic checked against data.len().
ring_buffer write_unaligned, overlay casts Head/count maintained modulo capacity. Offsets checked.
slot_map Overlay casts with generation counter Slot index validated.
bit_set None (all byte-level) N/A
sorted_vec read_unaligned, write_unaligned, copy_within Count validated, offsets checked. copy_within uses ptr::copy for overlapping regions.
journal write_unaligned, read_unaligned Cursor wraps within capacity. Bounds checked.
slab Offset casts, read_unaligned Bitmap allocation tracking. Slot offset validated against data length.
packed_map read_unaligned, write_unaligned Count validated, entry size arithmetic checked.

hopper-core::frame

phase.rs

Line Construct Invariant
borrow_mut borrow_unchecked_mut() via ExecutionContext Runtime borrow tracking via u64 bitmask (mutable_borrows). Each bit corresponds to an account index. AccountBorrowFailed returned on double-mutable-borrow.
borrow borrow_unchecked() Immutable borrow. No conflict tracking needed (follows Rust's shared-borrow model).

hopper-macros

hopper_layout!

Construct Invariant
unsafe impl Pod for $name Generated struct is #[repr(C)] over alignment-1 fields. Compile-time assertions enforce size_of == LEN and align_of == 1.
borrow_unchecked() / borrow_unchecked_mut() in load functions Protected by tiered validation: T1 checks owner + disc + version + layout_id + size before borrow. T2 checks owner + layout_id + size.
load_unchecked Explicitly marked unsafe fn. Caller assumes all risk.
load_unverified Size checked. Returns overlay even without full validation (tier 5 for indexers).

hopper_check!

Construct Invariant
borrow_unchecked() in disc/size arms Immutable borrow for validation reads. Called during resolve/validate phase (before any mutable borrows).

hopper-solana

token.rs, mint.rs

Line Construct Invariant
All pointer casts Data length >= TOKEN_ACCOUNT_LEN or MINT_LEN checked before cast. Target: Address (align 1).

cpi_guard.rs

Line Construct Invariant
71 instructions_sysvar.borrow_unchecked() Used to read the Instructions sysvar. Immutable, read-only.

typed_cpi.rs

Line Construct Invariant
298-299 borrow_unchecked() in checked_token_transfer Read-only borrows to compare mint fields before CPI. No conflicting mutable access at this point.

Audit Checklist

For any new unsafe added to the codebase, verify:

  • Bounds check precedes every pointer offset/cast
  • Target type is Pod (align-1, all bits valid)
  • // SAFETY: comment present and accurate
  • Mutable borrows tracked by frame bitmask or exclusive &mut access
  • No UB on the off-chain (non-SVM) path
  • target_os = "solana" gate if relying on SVM runtime layout

Unsafe Review Checklist (for auditors)

When reviewing Hopper code (or code that depends on Hopper), walk through these questions for every unsafe block:

  1. Is the target type alignment-1? Every Pod type in Hopper is #[repr(C)] or #[repr(transparent)] with all fields being [u8; N] or nested Pod types. If a new type is introduced, verify align_of == 1 with a compile-time assertion.

  2. Is the slice length checked before the cast? Every pod_from_bytes, overlay_at, and manual pointer cast must be preceded by data.len() >= T::SIZE or equivalent bounds arithmetic.

  3. Is aliasing structurally prevented? Mutable access must flow through either &mut self (compile-time) or the frame-level borrow bitmask (runtime). No two mutable references should be able to alias the same account data within a single instruction.

  4. Does it work off-chain? Code gated to target_os = "solana" may assume SVM account layout. Verify that the non-SVM path either provides equivalent safety or is unreachable.

  5. Is the // SAFETY: comment accurate and complete? It must state the precondition, why it holds, and what would go wrong if it didn't.

  6. Are MaybeUninit uses fully initialized before read? CPI builders use MaybeUninit arrays. Verify that add_account() is called for every slot before invoke().

  7. Does the test suite cover the boundary? Each unsafe boundary should have at least one test that exercises the happy path and one that exercises the rejection path (wrong size, wrong alignment, etc.).


Test Coverage by Danger Zone

Every module with unsafe blocks has corresponding tests that exercise the invariant boundaries. This table maps each risk area to its test coverage.

Module Risk Key Invariant Test Coverage
abi::integers Wire type soundness align == 1, size == WIRE_SIZE Compile-time assertions + prop_wire_* property tests
abi::typed_address Address cast soundness Address is [u8; 32], read-only prop_typed_address_* property tests
abi::fingerprint Deterministic hashing SHA-256 prefix must change with schema fingerprint_* golden tests in trust_tests
account::pod Overlay cast bounds data.len() >= T::SIZE before cast prop_pod_* + compile-time size_of assertions
account::segment Segment offset math Bounds checked before every cast segment_* trust tests + property tests
account::registry Registry pointer offset All offsets validated against data.len() registry_* trust tests
check::mod Sysvar instruction parsing Offset table + per-ix layout fidelity cpi_guard_* + sysvar_parse_* golden tests (with 0/1/N account metas)
check::fast RuntimeAccount header read SVM-only, gated to target_os = "solana" Relies on SVM runtime guarantees; untestable off-chain
cpi::mod MaybeUninit transmute All ACCTS slots initialized before transmute debug_assert_eq!(acct_cursor, ACCTS) + off-chain no-op path
cpi::mod CPI builder zeroed data InstructionAccount overwritten before invoke Off-chain path returns Ok(()), SVM path exercises full path
collections::journal Circular wrap + copy_nonoverlapping Head wraps within capacity, bounds checked journal_* trust tests: strict/circular, wrap-many, ordering, latest, out-of-bounds
collections::slab Bitmap + offset arithmetic Slot index validated, bounds checked slab_* trust tests: alloc/free cycle, double-free reject, full/realloc
collections::fixed_vec read_unaligned overlay Count/capacity validated fixed_vec_* unit tests
collections::ring_buffer write_unaligned overlay Head/count modulo capacity ring_buffer_* unit tests
collections::sorted_vec ptr::copy for insert/remove Count validated, offsets checked sorted_vec_* trust + property tests
frame::phase Borrow tracking bitmask u64 bitmask prevents double-mutable-borrow frame_* property tests
hopper-macros hopper_layout! Pod derivation Compile-time size_of == LEN, align_of == 1 Every generated type gets static assertions; used in all test layouts
hopper-solana::token Token account overlay data.len() >= TOKEN_ACCOUNT_LEN checked token_* integration tests
hopper-solana::cpi_guard Instructions sysvar borrow Immutable read for validation cpi_guard_* trust tests covering all guard variants
receipt Fingerprint hashing of account data FNV-1a deterministic, before/after tracked receipt_* trust tests + prop_receipt_* property tests

Boundary Test Files

The following dedicated test files exercise unsafe boundaries directly:

  • tests/unsafe_boundary_tests.rs - Pod from undersized/empty/oversized buffers, VerifiedAccount rejection, overlay-at OOB rejection, usize::MAX overflow check, header wire layout verification, segment descriptor boundary conditions, wire type roundtrips, unchecked cast parity.
  • tests/overlay_equivalence_tests.rs - pod_from_bytes vs pod_read value equivalence, VerifiedAccount::get() vs raw pod parity, overlay_at vs manual slice pod parity, cast_unchecked vs checked parity, mutable write-through equivalence, wire type overlay vs raw bytes, header overlay vs constructor.

Internal Hopper safety-review response (2026-04)

An internal safety review flagged four specific unsound or permissive surfaces. Earlier revisions incorrectly called that review an independent "Hopper Safety Audit" and linked an absent docs/Hopper Safety Audit.docx. No independent third-party audit was performed. This section records the internal findings, the action taken on each, and the invariants the fixes now enforce.

Finding 1. hopper-core::frame::{segment_ref, segment_mut, segment_mut_unchecked} returned naked references to T after dropping the backing byte-slice borrow

Fix landed: crates/hopper-core/src/frame/mod.rs now returns hopper_runtime::Ref<'_, T> / RefMut<'_, T> projected through the live byte-slice guard via Ref::project / RefMut::project. The returned guard owns the account's borrow state byte. it is released only when the typed reference drops, not when the function returns.

Invariant enforced: borrow state byte always matches the set of live Ref<T> / RefMut<T> guards returned from Frame.

Regression tests: frame::audit_tests::frame_segment_mut_writes_through_ref_mut, frame::audit_tests::frame_segment_ref_returns_live_guard.

Finding 2. T: Copy bound on hot-path access was too loose

bool, char, &T, NonZeroU64, and padded #[repr(C)] structs all satisfy Copy but are not safe to overlay on arbitrary bytes.

Fix landed: the canonical Pod trait now lives at the substrate layer (hopper_native::Pod) with the contract documented as four explicit obligations (all bit patterns valid, alignment-1, no padding, no interior pointers). hopper-runtime re-exports it (hopper_runtime::Pod) when the native backend is active; hopper-core re-exports it as hopper_core::account::Pod. Every hot-path access API tightened from T: Copy to T: Pod:

  • hopper_native::AccountView::{segment_ref, segment_mut, segment_ref_unchecked, segment_mut_unchecked, raw_ref, raw_mut}
  • hopper_runtime::AccountView::{segment_ref, segment_mut, segment_ref_const, segment_mut_const, segment_ref_typed, segment_mut_typed, raw_ref, raw_mut}
  • hopper_runtime::Context::{segment_ref, segment_mut, segment_ref_const, segment_mut_const, segment_ref_typed, segment_mut_typed, raw_ref, raw_mut, raw_unchecked, read_data}
  • hopper_core::frame::Frame::{segment_ref, segment_mut, segment_mut_unchecked}
  • Macro-generated __SegT escapes from #[hopper::context]

Finding 3. Projectable trait too permissive (Copy + 'static)

Fix landed: crates/hopper-native/src/project.rs now documents Projectable as the Tier-C unsafe escape hatch kept for backward compatibility with already-published programs. A strengthened SafeProjectable trait + project_safe / project_safe_mut helpers reject zero-sized overlays at compile time and steer all new code toward the Pod-bounded path. hopper-native::lens::read_field_pod added as a drop-in Pod-bounded replacement for read_field.

Finding 4. CLI/IDL/DX gaps

Fix landed: #[hopper::pod] standalone attribute macro (crates/hopper-macros-proc/src/pod.rs) lets any #[repr(C)] struct opt into the full contract without the #[hopper::state] header/layout_id/schema machinery. CLI already has hopper compile --emit rust, hopper inspect, hopper explain, hopper client gen --ts, hopper client gen --kt, hopper manager … wiring (see tools/hopper-cli).

Compile-time fences

#[hopper::state] now emits three additional const _: () = assert!(...) fences on every generated layout:

Fence What it catches
align_of::<T>() == 1 #[repr(C)] struct with a non-alignment-1 field slipped in (e.g. raw u64 instead of WireU64)
size_of::<T>() == sum of field sizes Compiler-inserted padding between fields
size_of::<T>() > 0 Zero-sized overlay (projects to a dangling pointer)
DISC != 0 Zero discriminator cannot be distinguished from an uninitialized buffer

All four fire at type-check time. Malformed layouts never reach link.

Const-generic TypedSegment<T, const OFFSET: u32> (audit innovation item)

New crates/hopper-runtime/src/segment.rs introduces a zero-sized TypedSegment marker that bakes both the overlay type and the body offset into the type system. AccountView::segment_ref_typed / segment_mut_typed and Context::segment_ref_typed / segment_mut_typed take such a marker and lower to ptr + literal_offset SBF with a literal size bounds check. A compile-time const _: () proves the marker is zero-sized so passing it around is free.

Coordination with live borrow state (audit appendix)

Three Hopper-runtime regression tests lock in the cross-path coordination the audit wanted proven:

  • live_load_blocks_segment_mut. account.load::<T>() + subsequent segment_mut rejected via the native state byte.
  • live_load_mut_blocks_segment_ref. exclusive load_mut rejects a concurrent segment_ref even though they use different registries.
  • every_access_path_is_tracked. walks every safe access method and asserts each one blocks a conflicting follow-up.

These, together with the Frame audit regression tests, mean every safe access path in Hopper is now covered by at least one regression test that fails loudly if the coordination breaks.

  • tests/compat_regression_tests.rs - Append-safe addition detection, forbidden field rename/resize, field removal as breaking, compare_fields report accuracy, is_backward_readable / requires_migration correctness, receipt wire format encode/decode roundtrip, Phase/CompatImpact enum roundtrips, segment/field mask roundtrip, reserved byte verification.

Post-Audit Closure

Last focused verification refresh: 2026-05-25. Current proof lanes are listed in the Verification section; avoid treating this document as a frozen workspace-wide test-count snapshot.

This section enumerates every item in the April 2026 internal safety review and points at the source-of-truth closure in the current tree. It is the ground truth the audit will be compared against on re-review.

Must-fix (5 of 5. DONE)

# Audit item Closure
M1 Reject malformed duplicate-account indices crates/hopper-native/src/raw_input.rs:16-46, 112-114, 176-179 (malformed_duplicate_marker trap on any forward/self-ref); lazy.rs:231-233 mirrors for lazy parse; D3 fuzz target continuously adversarial-tests the invariant
M2 RAII segment leases crates/hopper-runtime/src/segment_lease.rs (SegmentLease/SegRef/SegRefMut with Drop); integrated into Frame::segment_ref/segment_mut at crates/hopper-core/src/frame/mod.rs:207-300; regression tests in trust_tests.rs
M3 Canonical wire-fingerprint layout identity crates/hopper-macros-proc/src/state.rs:373-467. canonical_wire_stem + hopper:wire:v2 descriptor, SHA-256-hashed; spelling-drift regression tests at state.rs:515-568
M4 Field-level Pod proof at macro expansion crates/hopper-macros-proc/src/pod.rs and src/state.rs now emit a __FieldPodProof<T: bytemuck::Pod + Zeroable> marker per field. a bare unsafe impl bytemuck::Pod is a rubber stamp that does not check fields, so this closes the hole rubber stamps left. Every field type is forced through the trait bound at expansion time
M5 Compile-fail doctests for negative proof crates/hopper-runtime/src/pod.rs carries 3 compile-fail doctests for non-Pod overlays; at the 2026-08-16 source snapshot the root tests/ui.rs suite has 17 compile-fail fixtures under tests/compile_fail/, while tests/hopper-trybuild/tests/ui/ has 14 fail fixtures and 6 pass fixtures. Treat these as dated counts; the commands in D2 are the evidence.

Should-fix (4 of 4. DONE)

# Audit item Closure
S1 Address fingerprint collision safety crates/hopper-runtime/src/segment_borrow.rs:45-67. fast-path 8-byte compare + full-address fallback at line 197, 212-213
S2 Retire Projectable/SafeProjectable split crates/hopper-native/Cargo.toml legacy-projectable feature; SafeProjectable marked Tier-C; ZeroCopy is the unified modern surface
S3 Tighten close / close_to preconditions crates/hopper-runtime/src/account.rs:764-783 (writable + owner + dest-writable checks); crates/hopper-native/src/account_view.rs:389-415 (System Program ID constant + doc clarity)
S4 Fix stale T: Copy docs where code requires T: Pod Audited across crates/; all zero-copy signature docs now say T: Pod (see crates/hopper-runtime/src/context.rs:229-244, account.rs:182-187)

Structural (3 of 4 DONE; 1 deferred with rationale below)

# Audit item Status
ST1 Unify trait model -> ZeroCopy -> WireLayout -> AccountLayout DONE. crates/hopper-runtime/src/zerocopy.rs defines the three-tier stack; blanket impls make every LayoutContract automatically an AccountLayout
ST2 Declarative account constraints DONE (parser + validation + lifecycle). crates/hopper-macros-proc/src/context.rs parses init/init_if_needed/zero/close/realloc/realloc_payer/realloc_zero/payer/space/seeds/bump/has_one/owner/address/constraint, emits ordered validation, and generates lifecycle helpers. Signer<'info>, Account<'info, T>, and the other public typed wrappers are shipped; they are not deferred work.
ST3 Schema epoch in header + wire fingerprinting DONE. HopperHeader::schema_epoch: u32 at bytes 12-15; AccountLayout::WIRE_FINGERPRINT: u64 constant
ST4 hopper compile beyond --emit rust DONE. hopper compile --emit routes Rust preview, TypeScript, Kotlin, Python, Go, C, Rust client, IDL, Codama, and schema output through the shared manifest-source path

DX (1 of 4 DONE; 3 documented)

# Audit item Status
DX1 End-to-end build/test/deploy CLI DONE. tools/hopper-cli/src/cmd/lifecycle.rs:86-246
DX2 Cleaner generated access surfaces DONE. #[hopper::state] now emits {FIELD}_ABS_OFFSET: u32 inherent consts that fold HEADER_LEN + offset; callers pass them to typed-segment escapes directly without arithmetic boilerplate. Regression tests at examples/hopper-proc-vault/src/lib.rs:abs_offset_tests
DX3 Authored-source output pipeline DONE for the shipped Rust-authoring path. Source-derived manifest emission feeds IDL, Codama, schema, and client outputs through hopper compile --emit. This is not a claim that Hopper implements a separate authored programming language.
DX4 Canonical account/context syntax with full PDA/init/realloc/close/payer/space DONE via ST2 closure. every audit-listed attribute now lowers through #[hopper::context]

Docs and tests (2 of 4 DONE; 2 documented)

# Audit item Status
D1 Canonical unsafe-invariants document DONE. this file
D2 Compile-fail coverage DONE. Run cargo test --test ui --features proc-macros --locked and cargo test -p hopper-trybuild --locked. The dated 2026-08-16 source snapshot contains 17 root fail fixtures plus 14 fail and 6 pass trybuild fixtures; the commands, not the rolling counts, are the release evidence.
D3 Fuzzing low-level loaders/parsers DONE. fuzz/ has 5 targets at the 2026-08-16 snapshot: fuzz_instruction_frame, fuzz_decode_header, fuzz_decode_segments, fuzz_pod_overlay, and fuzz_account_view_load. The safe bounds-checked parse_instruction_frame_checked parser has its own regression tests.
D4 Program measurement fixtures Compiled ELF, account-state, rollback, and dated devnet checks; see program measurements.

Innovations (5 of 5 DONE)

# Audit innovation Status
I1 Borrow stack with typed leases DONE. SegmentLease / SegRef / SegRefMut RAII stack in crates/hopper-runtime/src/segment_lease.rs
I2 Generated typed-segment tokens everywhere DONE. {FIELD}_OFFSET, {FIELD}_ABS_OFFSET, {FIELD}_SIZE, {FIELD}_TYPE const emission from #[hopper::state]; #[hopper::context] consumes both
I3 Manifest-backed foreign account lenses DONE. crates/hopper-runtime/src/foreign.rs. ForeignManifest + ForeignLens<T> with four-step verification (owner / disc / wire_fp / schema_epoch range)
I4 Schema epoch with in-place migration helpers DONE. #[hopper::migrate(from, to)] proc macro in crates/hopper-macros-proc/src/migrate.rs + hopper::layout_migrations! composition macro + apply_pending_migrations runtime in crates/hopper-runtime/src/migrate.rs. 8 integration tests in tests/migrate_integration.rs
I5 Hybrid serialization (fixed body + typed dynamic tail) DONE. #[hopper::state(dynamic_tail = T)] + TailCodec trait (Borsh-subset) in crates/hopper-runtime/src/tail.rs. 12 codec + 8 integration tests

Follow-up design closure

On top of the original audit, a follow-up design pass called for the Jiminy-replacement safety surface, the hopper verify ABI-integrity command, and client-side layout verification. All three are now in-tree:

Design item Closure
require! / require_eq! crates/hopper-runtime/src/lib.rs
require_neq! crates/hopper-runtime/src/lib.rs
require_keys_eq! / require_keys_neq! (Jiminy-familiar) crates/hopper-runtime/src/lib.rs
require_gte! / require_gt! crates/hopper-runtime/src/lib.rs
check_signer / check_owner / check_writable free fns crates/hopper-core/src/check/mod.rs (pre-existing)
check_program(account, program_id) free fn crates/hopper-core/src/check/mod.rs (added post-audit)
checked_mul_div / checked_mul_div_ceil safe math crates/hopper-core/src/math/mod.rs (pre-existing)
hopper verify CLI tools/hopper-cli/src/cmd/verify.rs (manifest integrity + binary scan)
#[used] LAYOUT_ID anchor crates/hopper-macros-proc/src/state.rs emits per-layout static
Client-side assertLayoutId(data, hex) crates/hopper-schema/src/clientgen.rs TS generator
Per-layout assert{Name}Layout(data) helpers Same generator, paired with {NAME}_LAYOUT_ID const
hopper init scaffold tools/hopper-cli/src/cmd/lifecycle.rs::cmd_init (pre-existing)
Rust off-chain client generator crates/hopper-schema/src/rust_client.rs - RsClientGen emits ClientError, assert_{name}_layout, decode_{name}, {Ix}_ix builders; wired as hopper compile --emit rust-client; 9 regression tests
Token CPI signer-pre-check default crates/hopper-runtime/src/token.rs - Transfer/MintTo/Burn/CloseAccount/Approve/Revoke invoke() runs require_authority_signed_direct before the CPI so a missing signer surfaces MissingRequiredSignature instead of an opaque CPI error
SPL *Checked builders (Token-2022 extension safety) crates/hopper-runtime/src/token.rs - TransferChecked (idx 12), ApproveChecked (idx 13), MintToChecked (idx 14), BurnChecked (idx 15) carry a decimals: u8 byte the SPL token program validates against the mint's stored decimals. Every invoke() applies the same signer pre-check. 7 wire-format regression tests lock the byte layout
Deprecation of unchecked token builders crates/hopper-runtime/src/token.rs + crates/hopper-solana/src/typed_cpi.rs - Transfer, MintTo, Burn, Approve structs and their token_* wrapper functions marked #[deprecated(note = "use *Checked for Token-2022 safety")]. New token_transfer_checked, token_transfer_checked_signed, token_mint_to_checked, token_mint_to_checked_signed, token_burn_checked, token_burn_checked_signed, token_approve_checked free functions expose the safe path in hopper-solana
Typed-context validation binding (audit final-API Step 7) crates/hopper-macros-proc/src/program.rs::handler_invocation emits <ContextSpec>::bind(ctx)? or bind_with_args(...) for typed Ctx<Spec> handlers before calling their body. Binding performs the checks declared by that context. Raw &mut Context handlers receive the raw context and own their validation, regardless of the strict intent flag.
Sealed ZeroCopy trait (audit final-API Step 5) crates/hopper-runtime/src/zerocopy.rs::__sealed::HopperZeroCopySealed gates the blanket ZeroCopy impl. Every Hopper-authored surface stamps the seal: #[hopper::pod], #[hopper::state], hopper_layout! (both forms), and every primitive wire type (Wire{U,I}{8,16,32,64,128}, WireBool, TypedAddress<T>, UntypedAddress, AccountHeader, SegmentDescriptor, Address, plus the u8/u64/[u8; N] framework primitives). A user bypassing the macros with a bare unsafe impl Pod for Foo does not pick up ZeroCopy automatically. They would have to explicitly name the doc-hidden __sealed::HopperZeroCopySealed path, which signals the opt-out is deliberate
Canonical segment-access path documented crates/hopper-runtime/src/context.rs - rustdoc table on Context::segment_ref points callers to segment_ref_typed as the compile-time-offset canonical; runtime-offset variant stays available for dynamic iteration
Compile-proven borrow-guard constraint (audit Finding 2) HopperRefOnly in crates/hopper-runtime/src/ref_only.rs is a sealed marker trait implemented only by Ref, RefMut, SegRef, SegRefMut. An API bounded by G: HopperRefOnly rejects naked &T / &mut T at compile time. The seal lives in a private sealed module so downstream crates cannot stamp the marker onto arbitrary types. Closure test: tests/compile_fail/ref_only_rejects_raw_ref.rs captures rustc's error[E0277]: the trait bound '&mut u64: HopperRefOnly' is not satisfied verbatim
Policy-driven zero-copy runtime (strict / sealed / raw) HopperProgramPolicy in crates/hopper-runtime/src/policy.rs ships three named modes (STRICT, SEALED, RAW) plus custom lever-by-lever overrides. #[hopper::program(strict)] / (raw) / (sealed) / (allow_unsafe = false, ...) parses the attribute in crates/hopper-macros-proc/src/program.rs::parse_program_policy and emits pub const HOPPER_PROGRAM_POLICY: HopperProgramPolicy = ...; inside the module. When allow_unsafe = false, every handler gets #[deny(unsafe_code)] unless it opts back in via #[instruction(N, unsafe_memory)], per-handler const <HANDLER>_POLICY: HopperInstructionPolicy captures the override. Three-mode demonstration: examples/hopper-policy-vault/src/lib.rs with compile-time assert! blocks that lock the emitted constants to the named policies
Canonical raw-pointer escape hatch on Context crates/hopper-runtime/src/context.rs::Context::as_mut_ptr and as_ptr expose the explicit unsafe fn / safe-to-obtain pointer primitive the audit names. as_mut_ptr requires require_writable; as_ptr requires check_borrow. Both yield pointers into the loader-provided per-account buffer, transferring alias-safety to the caller as documented. Exercised by examples/hopper-policy-vault::raw_vault::raw_pointer_reset, which writes directly to a field at a compile-computed offset in raw mode
Explicit token-authority pre-checks (enforce_token_checks is intent) crates/hopper-runtime/src/token.rs::require_token_authority compares the token account's owner field at bytes [32..64] with the authority address; mismatch returns IncorrectAuthority. It does not establish token-program ownership, validate the complete layout, or check signer privilege. TransferChecked, BurnChecked, and ApproveChecked call it from invoke_strict() and invoke_signed_strict(). The direct strict methods also require signer privilege and return MissingRequiredSignature when absent. Signed strict methods pass PDA seeds for CPI runtime validation. The policy constant does not insert these calls. Owner-specific strict methods do not cover every valid delegate or multisig operation. The existing token-authority tests cover matching owner, mismatch, and short buffer cases.

Audit Provability

The three audit findings that remained open after the enforcement pass asked for invariants that are proven as well as enforced. Each row below names the exact file the auditor greps, what they see, and the compile-time failure mode a bypass produces:

Audit finding Grep target What an auditor sees Bypass failure mode
F1: provable single access path AccountView.*data_ptr_unchecked|borrow_unchecked Every slice-returning accessor on hopper_native::AccountView is pub unsafe fn (borrow_unchecked, borrow_unchecked_mut) or explicitly low-level raw pointer (data_ptr_unchecked) consumed by same-crate internals and the documented raw-pointer escape hatches in hopper-runtime. Safe paths (try_borrow, try_borrow_mut, segment_ref, segment_mut) return Ref / RefMut with native borrow-state tracking Any call to borrow_unchecked* requires an unsafe block visible in the caller's source; obtaining a raw pointer is spelled _unchecked and dereferencing it remains unsafe
F2: compile-proven borrow safety HopperRefOnly Eight impls total (four sealed-trait impls, four marker-trait impls), all visible in crates/hopper-runtime/src/ref_only.rs. No macro expansion, no derive. The compile-fail fixture tests/compile_fail/ref_only_rejects_raw_ref.rs is the end-to-end proof Raw reference at the call site: error[E0277]: the trait bound '&mut u64: HopperRefOnly' is not satisfied
F3: entrypoint and execution Compiled program fixtures Exact source and ELF hashes accompany the tested program. Reject unexpected state or failed validation; rerun after source changes.

hopper-native Unsafe Surface (post-audit supplement, R10)

The original audit scope centred on hopper-core, hopper-runtime, and hopper-solana. The hopper-native crate (raw substrate, loader parsing, syscall wrappers) was reviewed during the audit closure pass but its unsafe surface was not enumerated in this document at the same level of rigour. This section closes that gap and lists every unsafe entry point in hopper-native with its invariants and test coverage pointer. Paired with the existing table above, this makes UNSAFE_INVARIANTS.md the complete ground-truth inventory for auditors.

hopper-native/src/account_view.rs

Entry point Kind Invariant Test coverage
AccountView::new_unchecked(ptr) pub const unsafe fn ptr must point at a valid RuntimeAccount followed by at least data_len bytes, and stay dereferenceable for the lifetime of the view. Before any safe resize, the resize_delta slot must hold the data length from instruction entry. The entrypoint parsers (raw_input::deserialize_accounts, lazy::parse_one_account) establish both crates/hopper-native/tests/unsafe_accessors.rs::new_unchecked_views_the_header_and_data_it_was_given; crates/hopper-native/tests/loader_input_conformance.rs builds views through deserialize_accounts
AccountView::owner(&self) pub unsafe fn Returns &Address into the input buffer. The reference is invalid once the account is assigned or closed; the caller must not mutate the owner while it is live crates/hopper-native/tests/unsafe_accessors.rs::owner_reads_the_header_and_assign_replaces_it
AccountView::assign(&self, new_owner) pub unsafe fn The account must be writable and the transfer must be authorized by the current owner program. Writes the 32-byte owner slot and nothing else crates/hopper-native/tests/unsafe_accessors.rs::owner_reads_the_header_and_assign_replaces_it
AccountView::borrow_unchecked(&self) / borrow_unchecked_mut(&self) pub unsafe fn No conflicting borrow may be live. The slice is the same region the checked borrow returns, and the borrow_state byte is neither read nor written crates/hopper-native/tests/unsafe_accessors.rs::borrow_unchecked_is_the_checked_region_and_leaves_the_borrow_state_alone
AccountView::segment_ref_unchecked::<T>(offset) / segment_mut_unchecked::<T>(offset) pub unsafe fn offset + size_of::<T>() must not overflow and must be <= data_len; no conflicting borrow of the range may be live; for the mutable form the account must be writable. No bounds, size, writable, or borrow check runs. The checked segment_ref and segment_mut run all four crates/hopper-native/tests/unsafe_accessors.rs::unchecked_segments_address_the_same_bytes_as_the_checked_ones, crates/hopper-native/tests/unsafe_accessors.rs::checked_segments_refuse_what_the_unchecked_contract_assumes
AccountView::raw_ref::<T>() / raw_mut::<T>() pub unsafe fn T: Pod. The call goes through the checked segment path, so it is bounds checked and borrow tracked; it is unsafe because it overlays T with no layout or discriminator check crates/hopper-native/tests/unsafe_accessors.rs::raw_ref_and_raw_mut_are_bounds_checked_and_borrow_tracked
AccountView::resize_unchecked(new_len) pub unsafe fn The account must be writable, no data borrow may be live, and the growth over the entry length must be at most MAX_PERMITTED_DATA_INCREASE. Sets the length only: the caller zero-fills any grown region crates/hopper-native/tests/unsafe_accessors.rs::resize_unchecked_sets_the_length_and_nothing_else
AccountView::close_unchecked() pub unsafe fn No borrow of the account may be live. Sets lamports and data length to zero and the owner to the System Program. It moves no lamports to a recipient and does not zero the data; the checked close zeroes the data and refuses a live borrow crates/hopper-native/tests/unsafe_accessors.rs::close_unchecked_clears_lamports_length_and_owner_without_touching_data

hopper-native/src/raw_input.rs

Entry point Kind Invariant Test coverage
deserialize_accounts::<MAX>() pub unsafe fn input must be the pointer the Solana loader passes to the BPF entrypoint. Walks marker bytes and canonical RuntimeAccount frames with strict alignment parse_instruction_frame_checked tests (lines 475-543 in raw_input.rs) mirror the same parser and cover malformed, forward-reference, self-reference, and EOF inputs; Kani raw_input::kani_proofs (CI lane kani-native-rawinput-proofs, scripts/kani-native-rawinput.{sh,ps1}): stride_lemma_matches_checked_align_offset_formula proves the next_record_offset stride over the full loader-reachable range, and the differential_* harnesses prove the fused walk's slot pointers/count/instruction tail equal the safe oracle's for N <= 3 symbolic frames (including the skip-only tail beyond MAX)
deserialize_accounts_fast::<MAX>() pub unsafe fn Same as above, plus the caller must be running on a cluster where SIMD-0321 (the r2 instruction-data pointer) is active and pass the r2-derived data. Reached only through fast_entrypoint! built with the simd-0321 cargo feature, which also null-checks r2 and falls back to the scanning parser when the gate is inactive Same harness as eager variant; Kani differential_fast_walk_two_accounts + trap_fires_in_fast_walk_{dl0,dl_nonzero}
scan_instruction_frame() pub unsafe fn Input must be a valid BPF buffer. Returns the (program_id, data) pair without claiming any account slots; caller responsible for the subsequent scan parse_instruction_frame_checked tests; Kani differential_scan_frame_two_accounts + trap_fires_in_scan_frame_{dl0,dl_nonzero}
malformed_duplicate_marker(marker, slot) fn(..) -> ! Never returns. On-chain: calls sol_panic_. Off-chain: panics. Exists to close the pre-audit "Must-Fix #1" where an attacker-supplied forward duplicate reference fell through to account zero Covered by the forward-reference and self-reference fixtures in the checked-parser tests; Kani trap_fires_* should_panic harnesses run against buffers sized exactly to the encoded frame (any access one byte past it is a CBMC violation) and prove, per harness, that the trap is reachable (existential) and that no path in the assumed malformed-marker space has a non-panic failure (no OOB read/write, no invalid write, no overflow); the concrete trap_slot_zero_marker_* sub-harnesses are deterministic (single path), so their trap verdicts are universal for those marker values; oracle_rejects_exactly_the_malformed_markers (assert-based) proves the safe parser rejects every malformed marker universally. "The fused walk traps on every malformed marker on every path" is not machine-checked by any single harness; it follows from the combination spelled out in the family (c) comment in raw_input.rs

hopper-native/src/lazy.rs

Entry point Kind Invariant Test coverage
lazy_deserialize(input) pub unsafe fn Same input contract as deserialize_accounts. Returns a LazyContext whose cursor points at the next unparsed account frame; no account is materialized yet The test module of crates/hopper-native/src/lazy.rs: crates/hopper-native/src/lazy.rs::agrees_with_duplicates, crates/hopper-native/src/lazy.rs::agrees_every_data_len_residue, crates/hopper-native/src/lazy.rs::agrees_exactly_max_254_accounts compare it with the eager parser
LazyContext::parse_one_account() unsafe fn (private) The cursor must point at a duplicate marker or an account frame inside the input buffer, and at least one account must remain. next_account, skip, and drain_remaining check the remaining count before calling it crates/hopper-native/src/lazy.rs::skip_past_end_errors, crates/hopper-native/src/lazy.rs::drain_with_duplicates_aliases_canonical

hopper-native/src/pda.rs

Entry point Kind Invariant Test coverage
create_program_address syscall Seed-slice ABI and output buffer Seed count/length bounds are checked; SBF slice descriptors have the asserted 16-byte layout; the output has space for 32 bytes bench/framework-comparison/verifier/tests/pda_boundaries_sbf.rs
Inline unsafe in verify_program_address Bounded descriptor array and hash output Only the initialized descriptor prefix is exposed to the synchronous SHA-256 syscall; inputs remain live and the valid call fills the 32-byte output before it is read bench/framework-comparison/verifier/tests/pda_boundaries_sbf.rs
Inline unsafe in based_try_find_program_address Canonical descending bump search Each iteration creates an immutable bump byte, writes its descriptor, then borrows the initialized prefix for hashing. No shared bump reference is reused after mutation. The hash is initialized before curve validation or return bench/framework-comparison/verifier/tests/canonical_pda_sbf.rs and pda_boundaries_sbf.rs
Inline unsafe in find_bump_for_address Hash-match search without curve validation Uses the same per-iteration descriptor lifetime. Address equality alone proves neither off-curve membership nor canonicality; the caller must establish the required provenance separately bench/framework-comparison/verifier/tests/pda_boundaries_sbf.rs

hopper-native/src/mem.rs

Entry point Kind Invariant Test coverage
memcpy(dst, src, n) pub unsafe fn dst and src must be valid for n bytes and must not overlap. sol_memcpy_ on chain, core::ptr::copy_nonoverlapping off chain crates/hopper-native/tests/mem_primitives.rs::memcpy_copies_exactly_n_bytes
memmove(dst, src, n) pub unsafe fn Both regions valid for n bytes; they may overlap crates/hopper-native/tests/mem_primitives.rs::memmove_is_correct_when_the_regions_overlap
memset(dst, byte, n) pub unsafe fn dst valid for n bytes crates/hopper-native/tests/mem_primitives.rs::memset_fills_exactly_n_bytes
memcmp(a, b, n) pub unsafe fn returning Ordering Both pointers valid for n bytes crates/hopper-native/tests/mem_primitives.rs::memcmp_orders_by_the_first_differing_byte

hopper-native/src/cpi.rs (borrow-conflict invariant, expanded)

As of R7, cpi::invoke_unchecked and cpi::invoke_signed_unchecked now carry an explicit seven-item invariant list in their # Safety doc blocks. The inventory is:

  1. No aliasing borrows into any account in accounts for the call's duration.
  2. accounts corresponds to real accounts from the program entrypoint (address + signer/writable flags).
  3. Writability and signer flags match the instruction's declared requirements.
  4. Duplicate accounts impose caller responsibility for post-CPI re-borrow discipline.
  5. instruction.program_id / accounts / data pointers are valid for the call's lifetime.
  6. (signed variant) Every Signer seed derivation hashes to a signer address in accounts.
  7. (signed variant) Seed slice lifetimes exceed the call duration.

Checked variants (invoke, invoke_signed) enforce 2-4 and 6 before routing to the unchecked path; the typical caller should reach for those unless a CU measurement justifies bypassing validation.

Post-CPI validation is deliberately split by handle type:

  • ForeignLens<T> holds its account-data borrow for the lens lifetime, so every safe CPI tier refuses a writable hand-off until that lens is dropped.
  • ExternalAccount<T> repeats the adapter's owner, discriminator, length, and custom validation before every safe data, view, resolve, explain, or checked- proof issuance path.
  • InterfaceAccount<T> repeats owner-set and interface-layout validation before every typed load or resolution. A changed owner fails with IncorrectProgramId before bytes are interpreted.
  • Account<T> and InitAccount<T> are entry/lifecycle-bound roles. Generated validation initially pins Account<T> to the executing program and later typed loads recheck the Hopper header, but the wrappers do not retain an owner witness. Deref and as_account() deliberately expose the lower-level raw view. Solana prevents a foreign CPI from reassigning caller-owned state, so code that deliberately closes, reassigns, and recreates an account must stop using the old wrapper.
  • SystemAccount is also a point-in-time role and exposes no system-typed data. Recheck ownership after an intentional System Assign before relying on the role again.
  • ExternalChecked<T, P> revalidates the adapter's base owner/type contract on later view access, but proof-specific mint, authority, freshness, or other business facts are point-in-time evidence. They must not cross a CPI that can mutate the proven account; callers re-run checked() afterward. The unsafe unchecked CPI tier cannot enforce this rule.

Verification

cargo check --workspace --all-targets
cargo test  --workspace --no-fail-fast
cargo test --test ui --features proc-macros --locked
cargo test -p hopper-trybuild --locked
cargo test  --test require_macros        # 16 guard-macro tests pass
cargo test  --test migrate_integration --features proc-macros  # 8 migration-chain tests pass
cargo test  --test hybrid_tail_integration --features proc-macros  # 8 dynamic-tail tests pass
cargo test  -p hopper-schema rust_client # 9 Rust-client-gen tests pass
cd fuzz && cargo check                    # fuzz crate structure compiles
cargo build-sbf --manifest-path examples/hopper-parity-vault/Cargo.toml       # 15 KiB .so
cargo build-sbf --manifest-path examples/hopper-token-2022-vault/Cargo.toml   # 20 KiB .so
cargo run -p hopper-cli -- verify @examples/sample-manifest.json              # manifest integrity + binary scan
cargo run -p hopper-cli -- compile --emit rust-client @examples/sample-manifest.json  # Rust client emits

September 26 lifecycle correction (0.4.2)

Host borrow tracking did not exercise the former unchecked SBF segment path. Segment guards now retain the canonical native borrow as well as their range lease. Compiled v0/v3 regressions test conflicts through a second registry, whole-account access, resize, close, and checked CPI. Explicit disjoint splits retain one whole-account exclusive borrow. Construction is crate-private so unchecked offsets cannot reach the safe all_mut and get_mut methods.

Close operations preflight all applicable local checks before changing data or balances. Native borrow mappings transfer the same lease; a panicking mapping closure still drops the original guard during host unwinding. This targeted review and the generated inventory are not an independent security audit.