Skip to main content

hopper_native/
raw_input.rs

1//! Raw loader input parsing for Hopper Native.
2//!
3//! This is the single source of truth for Solana loader input decoding. It owns
4//! duplicate-account resolution, canonical-account lookup, and original-index
5//! tracking so higher layers operate on already-resolved account views.
6
7use core::mem::MaybeUninit;
8
9use crate::account_view::AccountView;
10use crate::address::Address;
11use crate::raw_account::RuntimeAccount;
12use crate::MAX_PERMITTED_DATA_INCREASE;
13
14const BPF_ALIGN_OF_U128: usize = 8;
15
16/// Malformed-input trap.
17///
18/// The Solana loader guarantees duplicate markers refer only to **earlier**
19/// account slots (Solana's account serialization documents the marker as
20/// "the index of the first account it is a duplicate of". necessarily a
21/// lower index). A forward-pointing marker therefore cannot be the result
22/// of a well-formed invocation: it either indicates a loader bug or
23/// adversarial input attempting to synthesize an aliasing `AccountView`.
24/// The earlier parser silently fell back to account zero (or null for
25/// slot 0), which produced either a null-pointer `AccountView` or an
26/// aliasing view to an unrelated account. We now trap immediately via
27/// `sol_panic_` (on Solana) so the transaction fails at parse time.
28#[inline(never)]
29#[cold]
30pub(crate) fn malformed_duplicate_marker(marker: u8, slot: usize) -> ! {
31    #[cfg(target_os = "solana")]
32    // SAFETY: `MSG` is a static byte string; the pointer and the length
33    // describe it.
34    unsafe {
35        // Keep the message short and on-chain-cheap. The loader log
36        // attaches the program id automatically.
37        const MSG: &[u8] = b"hopper: malformed duplicate marker";
38        crate::syscalls::sol_panic_(MSG.as_ptr(), MSG.len() as u64, slot as u64, marker as u64);
39    }
40    #[cfg(not(target_os = "solana"))]
41    {
42        panic!(
43            "hopper: malformed duplicate marker at slot {}: marker {} points forward",
44            slot, marker
45        );
46    }
47}
48
49/// Metadata for one parsed account slot in the loader input.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct RawAccountIndex {
52    /// Index of this slot in the original loader account array.
53    pub original_index: usize,
54    /// Canonical account index this slot resolves to, if duplicated.
55    pub duplicate_of: Option<usize>,
56}
57
58impl RawAccountIndex {
59    /// Whether this slot is a duplicate reference to an earlier account.
60    #[inline(always)]
61    pub const fn is_duplicate(&self) -> bool {
62        self.duplicate_of.is_some()
63    }
64}
65
66/// Instruction tail discovered after scanning the loader input buffer.
67#[derive(Clone)]
68pub struct RawInstructionFrame {
69    pub accounts_start: *mut u8,
70    pub account_count: usize,
71    pub instruction_data: &'static [u8],
72    pub program_id: Address,
73}
74
75/// Advance a record-start offset past one canonical account record.
76///
77/// Folds the entire per-account stride, 88-byte `RuntimeAccount` header,
78/// `data_len` bytes of account data, the `MAX_PERMITTED_DATA_INCREASE`
79/// realloc reserve, the u128 alignment padding, and the 8-byte rent-epoch
80/// tail, into one integer expression: adds plus one `and`-mask. This is
81/// the Pinocchio-shape stride and compiles to straight-line ALU ops,
82/// unlike `<*mut u8>::align_offset`, which the compiler cannot fold when
83/// the pointer's base alignment is opaque (~6 extra instructions per
84/// account).
85///
86/// Correctness of aligning the *relative* offset instead of the absolute
87/// address: the SVM loader serializes the input region at
88/// `MM_INPUT_START` (`0x4_0000_0000`; agave's `solana-sbpf`
89/// `ebpf::MM_INPUT_START`), so the buffer base is 8-aligned
90/// (`BPF_ALIGN_OF_U128`) and `offset % 8 == (base + offset) % 8`, the
91/// two formulations land on the same byte for every `data_len`. Because
92/// `RuntimeAccount::SIZE` (88), `MAX_PERMITTED_DATA_INCREASE` (10240),
93/// the rent-epoch tail (8), and the duplicate stride (8) are all
94/// multiples of 8, every record starts at an 8-aligned offset and only
95/// `data_len` contributes misalignment. Folding the trailing rent-epoch
96/// `+ 8` inside the round-up is exact since `8 ≡ 0 (mod 8)`:
97/// `((x + 8) + 7) & !7 == (((x + 7) & !7) + 8)`.
98///
99/// The walks use [`next_record`], this stride applied to a pointer; the
100/// offset form is the one the Kani stride lemma and the tests state, and
101/// `next_record_matches_the_offset_stride` pins the two together.
102#[cfg(any(test, kani))]
103#[inline(always)]
104const fn next_record_offset(offset: usize, data_len: usize) -> usize {
105    (offset
106        + RuntimeAccount::SIZE
107        + data_len
108        + MAX_PERMITTED_DATA_INCREASE
109        + 8
110        + (BPF_ALIGN_OF_U128 - 1))
111        & !(BPF_ALIGN_OF_U128 - 1)
112}
113
114/// Bytes from the start of a canonical record to the start of the next one,
115/// before rounding: the 88-byte `RuntimeAccount` header, the
116/// `MAX_PERMITTED_DATA_INCREASE` realloc reserve, the 8-byte rent epoch,
117/// and the 7 bytes of slack that turn the round-down in [`next_record`]
118/// into a round-up. The record's `data_len` is added on top.
119const CANONICAL_STRIDE: usize =
120    RuntimeAccount::SIZE + MAX_PERMITTED_DATA_INCREASE + 8 + (BPF_ALIGN_OF_U128 - 1);
121
122/// The record that follows the canonical record at `cursor`.
123///
124/// [`next_record_offset`] applied to a pointer. The loader's input region
125/// starts 8-aligned (`MM_INPUT_START`), so rounding the address and
126/// rounding the offset from the region start land on the same byte; the
127/// pointer form saves the base-plus-offset add every record access would
128/// otherwise pay. Two adds and a mask per record, the same shape as
129/// Pinocchio's walk.
130///
131/// # Safety
132///
133/// `cursor` is the start of a canonical record in a loader input buffer
134/// whose base is 8-aligned, and `data_len` is that record's data length.
135#[inline(always)]
136unsafe fn next_record(cursor: *mut u8, data_len: usize) -> *mut u8 {
137    // SAFETY: the loader serializes the whole record and then at least the
138    // 8-byte instruction-data length and the 32-byte program id, so the
139    // unrounded pointer, at most 7 bytes past the next record, stays inside
140    // the input buffer.
141    let unrounded = unsafe { cursor.add(CANONICAL_STRIDE + data_len) };
142    unrounded.map_addr(|address| address & !(BPF_ALIGN_OF_U128 - 1))
143}
144
145/// Materialize the canonical record at `cursor` into `slot` and return the
146/// start of the next record.
147///
148/// # Safety
149///
150/// `cursor` is a canonical (`0xFF`-marked) record boundary of an 8-aligned
151/// loader input buffer, the view has not escaped yet, and `slot` is
152/// writable.
153#[inline(always)]
154unsafe fn take_canonical<'info>(cursor: *mut u8, slot: *mut AccountView<'info>) -> *mut u8 {
155    let raw = cursor as *mut RuntimeAccount;
156    // SAFETY: `raw` is a canonical loader record (the caller's contract).
157    let view = unsafe { AccountView::new_unchecked(raw) };
158    // SAFETY: the view wraps the record just decoded and has not escaped,
159    // the contract of `initialize_original_data_len`.
160    unsafe { view.initialize_original_data_len() };
161    let data_len = view.data_len();
162    // SAFETY: the caller hands over a writable slot.
163    unsafe { slot.write(view) };
164    // SAFETY: `cursor` is the canonical record `data_len` describes.
165    unsafe { next_record(cursor, data_len) }
166}
167
168/// Resolve the duplicate marker at `cursor` (slot `slot`) to the earlier
169/// view it names and return the start of the next record. Traps on a
170/// marker that does not name an earlier slot.
171///
172/// Out of line and cold: the loader writes a duplicate only when an
173/// instruction names one account twice, so the canonical path stays
174/// straight and each unrolled slot pays one call site, not a copy of this
175/// body.
176///
177/// # Safety
178///
179/// `slots` holds initialized views in `0..slot`, `slots.add(slot)` is
180/// writable, and `cursor` is the duplicate record of slot `slot`.
181#[cold]
182#[inline(never)]
183unsafe fn take_duplicate<'info>(
184    cursor: *mut u8,
185    marker: u8,
186    slot: usize,
187    slots: *mut AccountView<'info>,
188) -> *mut u8 {
189    let duplicate_of = marker as usize;
190    // The marker must name a strictly earlier slot. Anything else (a
191    // forward reference, or any duplicate marker on slot 0, which has no
192    // earlier slot) is malformed loader input; trap rather than synthesize
193    // a null or aliasing view.
194    if duplicate_of >= slot {
195        malformed_duplicate_marker(marker, slot);
196    }
197    // SAFETY: `duplicate_of < slot`, so that slot was initialized earlier
198    // in this walk.
199    let raw = unsafe { (*slots.add(duplicate_of)).raw_ptr() };
200    // SAFETY: the caller hands over slot `slot` as writable, and `raw` came
201    // from a validated earlier slot of this frame.
202    unsafe { slots.add(slot).write(AccountView::new_unchecked(raw)) };
203    // A duplicate record is the marker byte and 7 bytes of padding.
204    // SAFETY: the loader serializes those 8 bytes, and more records or the
205    // instruction tail follow them.
206    unsafe { cursor.add(8) }
207}
208
209/// Materialize the record at `cursor` as slot `slot` and return the start
210/// of the next record.
211///
212/// # Safety
213///
214/// `cursor` is slot `slot`'s record boundary in an 8-aligned loader input
215/// buffer, `slot < MAX`, and slots `0..slot` are initialized.
216#[inline(always)]
217unsafe fn take_record<'info>(
218    cursor: *mut u8,
219    slot: usize,
220    slots: *mut AccountView<'info>,
221) -> *mut u8 {
222    // SAFETY: `cursor` is a record boundary, so its first byte is in bounds.
223    let marker = unsafe { *cursor };
224    if marker == NON_DUP_MARKER {
225        // SAFETY: a 0xFF marker opens a canonical record, and slot `slot`
226        // is inside the scratch array.
227        unsafe { take_canonical(cursor, slots.add(slot)) }
228    } else {
229        // SAFETY: the caller's contract, forwarded.
230        unsafe { take_duplicate(cursor, marker, slot, slots) }
231    }
232}
233
234/// The one materializing walk behind every scanning entrypoint.
235///
236/// Writes views for the leading records until `stop` of them are done or
237/// the compile-time bound runs out (`MAX`, and never past slot 253: the
238/// 254 materialization clamp the loader encoding sets, see
239/// [`deserialize_accounts`]). Returns how many views it wrote and the
240/// cursor after the last record it walked.
241///
242/// The first four slots are unrolled by hand behind compile-time guards,
243/// so a declared bound of up to four (`program_entrypoint!(process, 3)`)
244/// compiles to straight-line code: one compare per account against `stop`,
245/// constant scratch offsets, no slot counter and no back-edge. A duplicate
246/// marker, rare because the loader writes one only when an instruction
247/// names an account twice, leaves the unrolled prefix and continues in the
248/// one loop below, which also serves the slots past four that only a wider
249/// bound reaches. So the duplicate path is in the binary once, not once per
250/// unrolled slot.
251///
252/// # Safety
253///
254/// `cursor` is the first account record of an 8-aligned loader input
255/// buffer, and `stop` is at most the number of accounts the loader
256/// serialized.
257#[inline(always)]
258unsafe fn materialize<'info, const MAX: usize>(
259    mut cursor: *mut u8,
260    stop: usize,
261    accounts: &mut [MaybeUninit<AccountView<'info>>; MAX],
262) -> (usize, *mut u8) {
263    let limit = if MAX < MATERIALIZE_CLAMP {
264        MAX
265    } else {
266        MATERIALIZE_CLAMP
267    };
268    let slots = accounts.as_mut_ptr() as *mut AccountView<'info>;
269    let mut slot = 'canonical: {
270        macro_rules! unrolled_slot {
271            ($slot:literal) => {
272                // `limit` is a constant, so for a small bound the first test
273                // folds into an unconditional return after the last slot.
274                if $slot == limit || $slot == stop {
275                    return ($slot, cursor);
276                }
277                // SAFETY: `$slot < stop <= num_accounts` puts `cursor` on
278                // this slot's record boundary.
279                if unsafe { *cursor } != NON_DUP_MARKER {
280                    break 'canonical $slot;
281                }
282                // SAFETY: a 0xFF marker opens a canonical record, and
283                // `$slot < limit <= MAX` indexes the scratch array.
284                cursor = unsafe { take_canonical(cursor, slots.add($slot)) };
285            };
286        }
287        unrolled_slot!(0);
288        unrolled_slot!(1);
289        unrolled_slot!(2);
290        unrolled_slot!(3);
291        4usize
292    };
293    while slot < limit {
294        if slot == stop {
295            break;
296        }
297        // SAFETY: as in the unrolled slots: `slot < stop` puts `cursor` on
298        // a record boundary, `slot < limit <= MAX`, earlier slots are set.
299        cursor = unsafe { take_record(cursor, slot, slots) };
300        slot += 1;
301    }
302    (slot, cursor)
303}
304
305/// Views past this many slots are never materialized. Duplicate markers are
306/// one byte with 0xFF reserved for canonical records, so markers
307/// 0x00..=0xFE address slots 0..=254; materialization stops one below that
308/// encoding limit, as it always has, and slot 254 is walked skip-only.
309const MATERIALIZE_CLAMP: usize = 254;
310
311/// A canonical record's marker byte (the borrow state it starts in).
312const NON_DUP_MARKER: u8 = u8::MAX;
313
314/// Deserialize the loader input into `AccountView`s.
315///
316/// Duplicate-account resolution happens here. A duplicate slot reuses the
317/// canonical `RuntimeAccount` pointer of the earlier slot it references, and
318/// its `original_index` remains the loader slot where it appeared.
319///
320/// One pass over the account region: [`materialize`] writes a view for each
321/// of the first `MAX` records (never past slot 253) with a pointer cursor,
322/// and a skip-only walk carries the cursor over any records past the bound
323/// to the instruction data and program id. For a declared bound of three
324/// the walk is straight-line code, about eight instructions per account,
325/// one of them the store that records the account's original data length
326/// so every later `resize` is checked against it.
327///
328/// # Safety
329///
330/// `input` must point to a valid Solana BPF input buffer.
331#[inline(always)]
332pub unsafe fn deserialize_accounts<'info, const MAX: usize>(
333    input: *mut u8,
334    accounts: &mut [MaybeUninit<AccountView<'info>>; MAX],
335) -> (&'info Address, usize, &'info [u8]) {
336    // SAFETY: `input` points to the head of the Solana BPF input buffer,
337    // whose first 8 bytes are the account count. `read_unaligned` reads the
338    // u64 without assuming 8-byte pointer alignment.
339    let num_accounts = unsafe { core::ptr::read_unaligned(input as *const u64) as usize };
340    // SAFETY: the account records start right after the 8-byte count, and
341    // `num_accounts` is the loader's own count.
342    let (count, mut cursor) = unsafe { materialize::<MAX>(input.add(8), num_accounts, accounts) };
343
344    // Skip-only tail: records past the bound are not materialized, but the
345    // cursor must still cross them to reach the instruction data and
346    // program id. Only the record's size matters here: a duplicate is 8
347    // bytes whatever slot its marker names, and no view is made from it, so
348    // nothing can alias. The walk has no side effect, which lets the
349    // compiler drop it (and the last materialized record's stride) from a
350    // program that never reads its instruction data.
351    let mut slot = count;
352    while slot < num_accounts {
353        // SAFETY: `slot < num_accounts`, so `cursor` sits on a
354        // loader-produced record boundary within the input buffer.
355        let marker = unsafe { *cursor };
356        cursor = if marker == NON_DUP_MARKER {
357            // SAFETY: canonical record at a record boundary; its `data_len`
358            // header field is in bounds.
359            let data_len = unsafe { (*(cursor as *const RuntimeAccount)).data_len } as usize;
360            // SAFETY: `cursor` is the canonical record `data_len` describes.
361            unsafe { next_record(cursor, data_len) }
362        } else {
363            // SAFETY: a duplicate record is 8 bytes, and more records or
364            // the instruction tail follow it.
365            unsafe { cursor.add(8) }
366        };
367        slot += 1;
368    }
369
370    // Instruction tail: u64 LE length prefix, data bytes, 32-byte program id.
371    // SAFETY: the walk crossed all `num_accounts` records, so `cursor` is at
372    // the 8-byte instruction-data length.
373    let ix_data_len = unsafe { core::ptr::read_unaligned(cursor as *const u64) as usize };
374    // SAFETY: the loader serializes `ix_data_len` bytes right after the
375    // length prefix, then the program id.
376    let data = unsafe { cursor.add(8) };
377    // SAFETY: those bytes live for the whole invocation, matching the
378    // returned lifetime.
379    let instruction_data = unsafe { core::slice::from_raw_parts(data as *const u8, ix_data_len) };
380    // SAFETY: the 32-byte program id trails the instruction data; `Address`
381    // is a transparent `[u8; 32]` with alignment 1, so a reference into the
382    // buffer is valid at any offset. Handing out the reference instead of a
383    // copy saves a 32-byte stack spill.
384    let program_id: &'info Address = unsafe { &*(data.add(ix_data_len) as *const Address) };
385
386    (program_id, count, instruction_data)
387}
388
389/// The number of accounts the loader serialized: the first word of the
390/// input buffer. The count-exact entrypoint compares it with the matched
391/// arm's bound before walking anything, so accounts past the bound are
392/// refused instead of silently dropped.
393///
394/// # Safety
395///
396/// `input` must point at a loader-serialized input buffer (at least eight
397/// readable bytes).
398#[inline(always)]
399pub unsafe fn loader_account_count(input: *const u8) -> usize {
400    // SAFETY: the caller passes the loader input, whose first 8 bytes are
401    // the little-endian account count.
402    unsafe { core::ptr::read_unaligned(input as *const u64) as usize }
403}
404
405/// Materialize at most `MAX` leading account views without walking to the
406/// instruction tail.
407///
408/// For entrypoints that already hold the instruction data and program id
409/// (the SIMD-0321 `r2` pointer) and know how many accounts the matched
410/// instruction declares: `#[program(profile = "tiny")]` reads the
411/// discriminator first and materializes exactly that context's account
412/// count. Records past `MAX` are neither materialized nor walked, so the
413/// cost is the declared accounts only, and there is no pointer table to
414/// size for the transaction maximum. Duplicate markers inside the prefix
415/// are resolved exactly as [`deserialize_accounts`] resolves them; a
416/// duplicate can only reference an earlier slot, so no reference escapes
417/// the materialized prefix. `limit` is the matched instruction's bound (at
418/// most `MAX`, the widest bound in the program, so one walk serves every
419/// arm); the return value is the number of views written,
420/// `min(num_accounts, limit)`, and the caller's context binder enforces its
421/// own minimum.
422///
423/// # Safety
424///
425/// `input` must point to a valid Solana BPF input buffer.
426#[inline(always)]
427pub unsafe fn deserialize_leading_accounts<'info, const MAX: usize>(
428    input: *mut u8,
429    accounts: &mut [MaybeUninit<AccountView<'info>>; MAX],
430    limit: usize,
431) -> usize {
432    // SAFETY: `input` points to the head of the loader input buffer, whose
433    // first 8 bytes are the account count.
434    let num_accounts = unsafe { core::ptr::read_unaligned(input as *const u64) as usize };
435    let stop = if num_accounts > limit {
436        limit
437    } else {
438        num_accounts
439    };
440    // SAFETY: the records start after the 8-byte count, and `stop` is at
441    // most the loader's count.
442    let (count, _) = unsafe { materialize::<MAX>(input.add(8), stop, accounts) };
443    count
444}
445
446/// Fast two-argument deserialize: instruction data and program id are provided
447/// directly by the caller (from the SVM's second entrypoint register), so the
448/// walk stops at the last materialized account instead of crossing the rest.
449///
450/// Materializes the same views, with the same 254 clamp, as
451/// [`deserialize_accounts`]: this is the `r2` arm of one entrypoint whose
452/// null-check fallback is the scanning walk, so the two must report the
453/// same `count` for the same input, or the same binary's `accounts.len()`
454/// would depend on which arm ran.
455///
456/// # Safety
457///
458/// * `input` must point to a valid Solana BPF input buffer.
459/// * `ix_data` must point to the instruction data with its length stored as
460///   `u64` at offset `-8`.
461/// * `program_id` must be the correct program id for this invocation.
462#[inline(always)]
463pub unsafe fn deserialize_accounts_fast<'info, const MAX: usize>(
464    input: *mut u8,
465    accounts: &mut [MaybeUninit<AccountView<'info>>; MAX],
466    instruction_data: &'info [u8],
467    program_id: &'info Address,
468) -> (&'info Address, usize, &'info [u8]) {
469    // SAFETY: `input` points to the head of the Solana BPF input buffer, whose
470    // first 8 bytes are the account count. `read_unaligned` reads the u64
471    // without assuming 8-byte pointer alignment.
472    let num_accounts = unsafe { core::ptr::read_unaligned(input as *const u64) as usize };
473    // SAFETY: the records start after the 8-byte count, and `num_accounts`
474    // is the loader's own count.
475    let (count, _) = unsafe { materialize::<MAX>(input.add(8), num_accounts, accounts) };
476    (program_id, count, instruction_data)
477}
478
479// ── SIMD-0449: the pre-computed account-pointer table ────────────────
480//
481// SIMD-0449 has the runtime append a `[u64; num_accounts]` array of
482// account-record pointers to the input, after the instruction tail,
483// "regardless of whether it is read or not" and fully backwards
484// compatible, programs that keep scanning simply keep paying O(n).
485// Each entry is the address of a CANONICAL `RuntimeAccount` record,
486// pre-deduplicated by the runtime (a duplicate slot carries the same
487// pointer value as the slot it duplicates), so consuming it needs no
488// stride walk and no duplicate-marker resolution.
489//
490// Hopper is uniquely positioned to consume it: `AccountView` is one
491// raw `*mut RuntimeAccount` (const-asserted below), so the SIMD's
492// `[u64]` array IS a valid `[AccountView]`, resolution becomes a
493// single `from_raw_parts`, where an SDK `AccountInfo`
494// (`Rc<RefCell<…>>`) must still loop to construct each element.
495//
496// Table location (per the SIMD, relative to the SIMD-0321 r2
497// instruction-data pointer): the instruction tail is
498// `[ix_data][program_id: 32]`, and the table starts at the next
499// 8-aligned byte after it. The account COUNT stays where it always
500// was, the input buffer's first u64.
501//
502// The runtime feature gate is `ptr9umikaeAS7ZBBp2fsfRhie16F1V2jCKA2y6gXNAK`
503// (agave `direct_account_pointers_in_program_input`; NOTE the 2026-04-15
504// rekey in agave PR #11934, the original `ptrXWLk…` gate is dead, and the
505// same PR pinned each table entry to the account RECORD start, i.e. the
506// dup-marker/borrow byte where `RuntimeAccount` begins, which is exactly
507// what the overlay below casts). Activated on testnet and devnet; pending
508// mainnet-beta (min agave v4.1.0-beta.0), check `hopper feature-gate`.
509// These functions are compiled unconditionally (they are inert unless
510// called); the `simd-0449` cargo feature only flips
511// [`SIMD_0449_TABLE_ENABLED`], which `hopper_fast_entrypoint!` consults to
512// select the table path, a `const`, so the untaken branch folds away
513// entirely.
514
515/// Whether this build trusts the SIMD-0449 account-pointer table
516/// (`feature = "simd-0449"`). Enabling it before the SIMD activates on
517/// the target cluster reads garbage, ship it only alongside the
518/// cluster gate, exactly like `simd-0321`.
519pub const SIMD_0449_TABLE_ENABLED: bool = cfg!(feature = "simd-0449");
520
521/// Failure reported by the host/replay SIMD-0449 conformance decoder.
522///
523/// The on-chain fast path deliberately trusts the loader: the SVM constructs
524/// the pointer table and a program cannot alter it before entry. Replay tools,
525/// alternate SVMs, fuzzers, and fixture consumers do not get that trust for
526/// free, so [`deserialize_accounts_0449_checked`] validates the complete
527/// account walk and requires every table entry to equal the canonical record
528/// pointer the legacy ABI walk derives.
529#[derive(Clone, Copy, Debug, PartialEq, Eq)]
530pub enum DirectMappingError {
531    /// The input pointer was null.
532    NullInput,
533    /// Integer arithmetic over the supplied frame bounds overflowed.
534    ArithmeticOverflow,
535    /// The supplied byte length ends in the middle of an ABI component.
536    TruncatedInput,
537    /// The frame contains more accounts than the caller-provided output.
538    TooManyAccounts { count: usize, capacity: usize },
539    /// A duplicate marker did not refer to a strictly earlier slot.
540    MalformedDuplicate { slot: usize, duplicate_of: usize },
541    /// The caller's instruction-data slice is not the exact slice in `input`.
542    InstructionDataMismatch,
543    /// The computed pointer table is not aligned to an eight-byte boundary.
544    PointerTableMisaligned,
545    /// A table entry points outside the supplied input frame.
546    PointerOutOfBounds { slot: usize },
547    /// A table entry is not aligned like a canonical account record.
548    PointerMisaligned { slot: usize },
549    /// A table entry is in-bounds but does not name this slot's canonical
550    /// account record (including duplicate-slot canonicalization).
551    NonCanonicalPointer { slot: usize },
552}
553
554#[inline(always)]
555fn checked_end(offset: usize, size: usize, input_len: usize) -> Result<usize, DirectMappingError> {
556    let end = offset
557        .checked_add(size)
558        .ok_or(DirectMappingError::ArithmeticOverflow)?;
559    if end > input_len {
560        return Err(DirectMappingError::TruncatedInput);
561    }
562    Ok(end)
563}
564
565/// Validate and consume a SIMD-0449 account-pointer table.
566///
567/// This is the conformance/replay companion to
568/// [`deserialize_accounts_0449_into`]. It independently walks the legacy
569/// account section, validates every record boundary and duplicate marker,
570/// pins the caller-provided instruction-data slice to the frame, then checks
571/// every direct pointer against the canonical address derived by that walk.
572/// Only after all entries pass are `AccountView`s materialized into `accounts`.
573///
574/// The function is allocation-free and therefore usable by alternate SVM
575/// harnesses as well as ordinary host tests. It is intentionally not selected
576/// by the on-chain entrypoint: its full O(n) legacy-layout validation would
577/// discard SIMD-0449's O(1) pointer-resolution benefit. The production table
578/// path still performs the smaller per-account write required to capture safe
579/// resize baselines.
580///
581/// # Safety
582///
583/// `input..input + input_len` must be readable for the duration of the call.
584/// `instruction_data` must either point into that same allocation or the
585/// function returns [`DirectMappingError::InstructionDataMismatch`].
586pub unsafe fn deserialize_accounts_0449_checked<'info, const MAX: usize>(
587    input: *mut u8,
588    input_len: usize,
589    accounts: &mut [MaybeUninit<AccountView<'info>>; MAX],
590    instruction_data: &'info [u8],
591) -> Result<(Address, usize, &'info [u8]), DirectMappingError> {
592    if input.is_null() {
593        return Err(DirectMappingError::NullInput);
594    }
595    checked_end(0, 8, input_len)?;
596
597    let base = input as usize;
598    // SAFETY: the first eight bytes were checked above and the caller grants
599    // readability for the supplied frame.
600    let num_accounts = unsafe { core::ptr::read_unaligned(input as *const u64) as usize };
601    if num_accounts > MAX {
602        return Err(DirectMappingError::TooManyAccounts {
603            count: num_accounts,
604            capacity: MAX,
605        });
606    }
607
608    // One canonical byte offset per loader slot. A duplicate copies the
609    // offset of the earlier slot it names.
610    let mut canonical_offsets = [0usize; MAX];
611    let mut offset = 8usize;
612    let mut slot = 0usize;
613    while slot < num_accounts {
614        checked_end(offset, 1, input_len)?;
615        // SAFETY: the marker byte is within the validated frame.
616        let marker = unsafe { *input.add(offset) };
617        if marker == u8::MAX {
618            checked_end(offset, RuntimeAccount::SIZE, input_len)?;
619            canonical_offsets[slot] = offset;
620            // `data_len` is the final u64 in the 88-byte runtime header.
621            // SAFETY: the full header was bounds-checked above.
622            let data_len =
623                unsafe { core::ptr::read_unaligned(input.add(offset + 80) as *const u64) as usize };
624            let body_end = offset
625                .checked_add(RuntimeAccount::SIZE)
626                .and_then(|v| v.checked_add(data_len))
627                .and_then(|v| v.checked_add(MAX_PERMITTED_DATA_INCREASE))
628                .ok_or(DirectMappingError::ArithmeticOverflow)?;
629            // Canonical records carry padding to eight bytes and an eight-byte
630            // rent epoch. Express the alignment without pointer arithmetic so
631            // an adversarial length cannot create UB before it is rejected.
632            let aligned = body_end
633                .checked_add(BPF_ALIGN_OF_U128 - 1)
634                .ok_or(DirectMappingError::ArithmeticOverflow)?
635                & !(BPF_ALIGN_OF_U128 - 1);
636            offset = checked_end(aligned, 8, input_len)?;
637        } else {
638            let duplicate_of = marker as usize;
639            if duplicate_of >= slot {
640                return Err(DirectMappingError::MalformedDuplicate { slot, duplicate_of });
641            }
642            canonical_offsets[slot] = canonical_offsets[duplicate_of];
643            offset = checked_end(offset, 8, input_len)?;
644        }
645        slot += 1;
646    }
647
648    // Pin the instruction tail exactly. Supplying an equal byte string from a
649    // different allocation is insufficient: the table location is derived
650    // from the in-frame r2 slice under SIMD-0321/0449.
651    let ix_len_end = checked_end(offset, 8, input_len)?;
652    // SAFETY: the length prefix is inside the frame.
653    let ix_len = unsafe { core::ptr::read_unaligned(input.add(offset) as *const u64) as usize };
654    let ix_offset = ix_len_end;
655    let ix_end = checked_end(ix_offset, ix_len, input_len)?;
656    if instruction_data.as_ptr() as usize != base + ix_offset || instruction_data.len() != ix_len {
657        return Err(DirectMappingError::InstructionDataMismatch);
658    }
659
660    let program_end = checked_end(ix_end, 32, input_len)?;
661    // SAFETY: the complete 32-byte program id was bounds-checked.
662    let program_id = Address::new_from_array(unsafe {
663        core::ptr::read_unaligned(input.add(ix_end) as *const [u8; 32])
664    });
665    let table_offset = program_end
666        .checked_add(BPF_ALIGN_OF_U128 - 1)
667        .ok_or(DirectMappingError::ArithmeticOverflow)?
668        & !(BPF_ALIGN_OF_U128 - 1);
669    if !(base + table_offset).is_multiple_of(BPF_ALIGN_OF_U128) {
670        return Err(DirectMappingError::PointerTableMisaligned);
671    }
672    let table_bytes = num_accounts
673        .checked_mul(core::mem::size_of::<u64>())
674        .ok_or(DirectMappingError::ArithmeticOverflow)?;
675    checked_end(table_offset, table_bytes, input_len)?;
676
677    let frame_end = base
678        .checked_add(input_len)
679        .ok_or(DirectMappingError::ArithmeticOverflow)?;
680    slot = 0;
681    while slot < num_accounts {
682        // SAFETY: the whole table was checked above; read_unaligned keeps the
683        // conformance path correct even when the containing allocation has a
684        // weaker alignment than the real SVM mapping.
685        let pointer = unsafe {
686            core::ptr::read_unaligned(input.add(table_offset + slot * 8) as *const u64) as usize
687        };
688        let pointer_end = pointer
689            .checked_add(RuntimeAccount::SIZE)
690            .ok_or(DirectMappingError::ArithmeticOverflow)?;
691        if pointer < base || pointer_end > frame_end {
692            return Err(DirectMappingError::PointerOutOfBounds { slot });
693        }
694        if pointer % BPF_ALIGN_OF_U128 != 0 {
695            return Err(DirectMappingError::PointerMisaligned { slot });
696        }
697        let expected = base
698            .checked_add(canonical_offsets[slot])
699            .ok_or(DirectMappingError::ArithmeticOverflow)?;
700        if pointer != expected {
701            return Err(DirectMappingError::NonCanonicalPointer { slot });
702        }
703        slot += 1;
704    }
705
706    // Materialize only after the full table validates, so a failure never
707    // leaves a partially trusted output slice.
708    slot = 0;
709    while slot < num_accounts {
710        let pointer = base + canonical_offsets[slot];
711        // SAFETY: this pointer was derived from a bounds-checked canonical
712        // header and its corresponding table entry matched exactly.
713        let view = unsafe { AccountView::new_unchecked(pointer as *mut RuntimeAccount) };
714        // SAFETY: full validation above proved this is a canonical loader
715        // record and no materialized view has escaped yet.
716        unsafe { view.initialize_original_data_len() };
717        accounts[slot] = MaybeUninit::new(view);
718        slot += 1;
719    }
720
721    Ok((program_id, num_accounts, instruction_data))
722}
723
724// Layout precondition for the table cast, checked at compile time: an
725// `AccountView` must be exactly one 8-byte pointer for `[u64; n]` to
726// reinterpret as `[AccountView; n]`.
727const _: () = assert!(
728    core::mem::size_of::<AccountView<'static>>() == 8
729        && core::mem::align_of::<AccountView<'static>>() == 8,
730    "AccountView must stay a single 8-byte pointer for the SIMD-0449 table cast"
731);
732
733/// SIMD-0449 direct account resolution: overlay the runtime's appended
734/// account-pointer table as a borrowed `[AccountView]`, one bounds
735/// computation and one `from_raw_parts`, then capture each account's
736/// invocation-wide resize baseline.
737///
738/// Pointer resolution itself is O(1). Safe account resizing requires one
739/// tiny write per account because ABIv1 serializes zero padding in the
740/// original-length slot; this matches the scanning entrypoint.
741///
742/// # Safety
743///
744/// * `input` must point to a valid Solana BPF input buffer.
745/// * `instruction_data` must be the loader-serialized instruction data
746///   for this invocation (as delivered via the SIMD-0321 `r2`
747///   register), with the 32-byte program id trailing it.
748/// * The SIMD-0449 table MUST actually be present; i.e. the SIMD is
749///   active on the executing cluster. Calling this where the runtime
750///   did not serialize the table reads unrelated bytes past the
751///   program id.
752#[inline(always)]
753pub unsafe fn deserialize_accounts_0449<'info>(
754    input: *mut u8,
755    instruction_data: &'info [u8],
756) -> &'info [AccountView<'info>] {
757    // SAFETY: the input buffer's first 8 bytes are the account count,
758    // unchanged by SIMD-0449.
759    let num_accounts = unsafe { core::ptr::read_unaligned(input as *const u64) as usize };
760    // Table start: first 8-aligned byte after `[ix_data][program_id]`.
761    let tail_end = instruction_data.as_ptr() as usize + instruction_data.len() + 32;
762    let table = ((tail_end + (BPF_ALIGN_OF_U128 - 1)) & !(BPF_ALIGN_OF_U128 - 1))
763        as *const AccountView<'info>;
764    // SAFETY: with the SIMD active, the runtime serialized exactly
765    // `num_accounts` pre-deduplicated canonical record pointers at
766    // `table`; the layout const-assert above proves `AccountView` is
767    // pointer-shaped, and the buffer outlives `'info`.
768    let views = unsafe { core::slice::from_raw_parts(table, num_accounts) };
769    let mut slot = 0usize;
770    while slot < num_accounts {
771        // SAFETY: every table entry is a loader-provided canonical record
772        // pointer and initialization occurs before the returned slice escapes.
773        unsafe { views.get_unchecked(slot).initialize_original_data_len() };
774        slot += 1;
775    }
776    views
777}
778
779/// Adapter matching the `deserialize_accounts_fast` shape: copy up to
780/// `MAX` table entries into the caller's array (8 bytes per account,
781/// a pointer copy, not a record parse) so the existing entrypoint
782/// plumbing consumes the table without changing its account storage.
783///
784/// # Safety
785///
786/// Same contract as [`deserialize_accounts_0449`]; additionally
787/// `program_id` must be the correct program id for this invocation.
788#[inline(always)]
789pub unsafe fn deserialize_accounts_0449_into<'info, const MAX: usize>(
790    input: *mut u8,
791    accounts: &mut [MaybeUninit<AccountView<'info>>; MAX],
792    instruction_data: &'info [u8],
793    program_id: &'info Address,
794) -> (&'info Address, usize, &'info [u8]) {
795    // SAFETY: forwarded caller contract.
796    let table = unsafe { deserialize_accounts_0449(input, instruction_data) };
797    // Same 254 materialization clamp as the scanning walk and the r2 fast
798    // path: all three are arms of one entrypoint and must report the same
799    // `count` for the same input (see `deserialize_accounts_fast`).
800    let addressable = if table.len() > 254 { 254 } else { table.len() };
801    let count = addressable.min(MAX);
802    let mut slot = 0usize;
803    while slot < count {
804        // SAFETY: `slot < count <= MAX` and `slot < table.len()`.
805        unsafe {
806            *accounts.get_unchecked_mut(slot) = MaybeUninit::new(table.get_unchecked(slot).clone());
807        }
808        slot += 1;
809    }
810    (program_id, count, instruction_data)
811}
812
813/// Parse just the instruction tail and account span from the loader input.
814///
815/// This supports both eager entrypoint parsing and lazy account iteration.
816/// The returned frame carries the original account span start so duplicate and
817/// canonical-account relationships remain defined at the loader level.
818///
819/// # Safety
820///
821/// `input` must point to a valid Solana BPF input buffer.
822#[inline(always)]
823pub unsafe fn scan_instruction_frame(input: *mut u8) -> RawInstructionFrame {
824    let mut scan = input;
825
826    // SAFETY: `scan` starts at the head of the Solana BPF input buffer, whose
827    // first 8 bytes are the account count. `read_unaligned` avoids assuming the
828    // pointer is 8-byte aligned.
829    let num_accounts = unsafe { core::ptr::read_unaligned(scan as *const u64) as usize };
830    // SAFETY: advancing past the 8-byte account-count prefix keeps `scan`
831    // within the loader input buffer, at the first account record boundary.
832    scan = unsafe { scan.add(8) };
833    let accounts_start = scan;
834
835    let mut slot = 0usize;
836    while slot < num_accounts {
837        // SAFETY: `scan` walks the loader's input buffer record by record,
838        // using the loader's own stride, for the `num_accounts` records the
839        // loader serialized; the instruction data, its length word, and the
840        // program id follow the last record in that buffer.
841        let marker = unsafe { *scan };
842        if marker == u8::MAX {
843            let raw = scan as *const RuntimeAccount;
844            // SAFETY: `scan` walks the loader's input buffer record by
845            // record, using the loader's own stride, for the `num_accounts`
846            // records the loader serialized; the instruction data, its length
847            // word, and the program id follow the last record in that buffer.
848            let data_len = unsafe { (*raw).data_len as usize };
849            let mut step = RuntimeAccount::SIZE + data_len + MAX_PERMITTED_DATA_INCREASE;
850            step += unsafe { scan.add(step).align_offset(BPF_ALIGN_OF_U128) };
851            step += 8;
852            // SAFETY: `scan` walks the loader's input buffer record by
853            // record, using the loader's own stride, for the `num_accounts`
854            // records the loader serialized; the instruction data, its length
855            // word, and the program id follow the last record in that buffer.
856            scan = unsafe { scan.add(step) };
857        } else {
858            let duplicate_of = marker as usize;
859            if duplicate_of >= slot {
860                malformed_duplicate_marker(marker, slot);
861            }
862            // SAFETY: Duplicate-account entries are 8-byte slots in the
863            // Solana input frame format; scanner bounds are driven by
864            // `num_accounts` and validated traversal above.
865            scan = unsafe { scan.add(8) };
866        }
867        slot += 1;
868    }
869
870    // SAFETY: `scan` now points at the 8-byte instruction-data length in the
871    // Solana BPF input buffer. `read_unaligned` avoids assuming 8-byte pointer
872    // alignment of `scan`.
873    let data_len = unsafe { core::ptr::read_unaligned(scan as *const u64) as usize };
874    scan = unsafe { scan.add(8) };
875    let instruction_data = unsafe { core::slice::from_raw_parts(scan as *const u8, data_len) };
876    // SAFETY: `scan` walks the loader's input buffer record by record, using
877    // the loader's own stride, for the `num_accounts` records the loader
878    // serialized; the instruction data, its length word, and the program id
879    // follow the last record in that buffer.
880    scan = unsafe { scan.add(data_len) };
881
882    let program_id_ptr = scan as *const [u8; 32];
883    // SAFETY: `scan` walks the loader's input buffer record by record, using
884    // the loader's own stride, for the `num_accounts` records the loader
885    // serialized; the instruction data, its length word, and the program id
886    // follow the last record in that buffer.
887    let program_id = Address::new_from_array(unsafe { *program_id_ptr });
888
889    RawInstructionFrame {
890        accounts_start,
891        account_count: num_accounts.min(254),
892        instruction_data,
893        program_id,
894    }
895}
896
897// =====================================================================
898// Safe bounds-checked loader-input parser (fuzz and off-chain harness).
899// =====================================================================
900//
901// The primary parser above is a pure-pointer fast path: on-chain it
902// consumes an SVM-loaded byte buffer whose layout is guaranteed by the
903// loader. Off-chain tools (`hopper dump`, `hopper test`, fuzz harnesses,
904// RPC decoders) do **not** have that guarantee. they receive arbitrary
905// byte slices. Feeding one to `scan_instruction_frame` would invite OOB
906// reads on any short / truncated input.
907//
908// `parse_instruction_frame_checked` is the safe companion: it walks a
909// `&[u8]` using a bounds-checked cursor and returns structured
910// `Result<FrameInfo, FrameError>`. It enforces exactly the same
911// duplicate-marker well-formedness rules (forward references are
912// rejected, not silently-aliased) and the same loader framing (88-byte
913// `RuntimeAccount` header, `MAX_PERMITTED_DATA_INCREASE` reserve, u128
914// alignment padding, `rent_epoch` tail, instruction_data with u64-LE
915// length prefix, 32-byte program id trailer).
916
917/// Hard cap on accounts the safe parser will record slot offsets for.
918///
919/// Matches Solana's own 256-account cap per instruction. Buffers that
920/// declare more than this are rejected with
921/// [`FrameError::AccountCountOutOfRange`].
922pub const MAX_SAFE_ACCOUNT_SLOTS: usize = 256;
923
924/// Summary of a safely-parsed loader input frame.
925///
926/// Only metadata is returned. the full `AccountView` construction
927/// requires the raw pointer path. This struct is what off-chain tools
928/// (and fuzz harnesses) need to verify a buffer is well-formed.
929///
930/// The `slot_offsets` array is a fixed `[usize; MAX_SAFE_ACCOUNT_SLOTS]`
931/// with the first `account_count` entries populated. Remaining entries
932/// are zero. Callers can distinguish duplicate vs canonical slots by
933/// checking whether `buffer[offset]` equals `0xFF`.
934#[derive(Clone, Debug, PartialEq, Eq)]
935pub struct FrameInfo {
936    /// Number of accounts the loader would hand to the program.
937    pub account_count: usize,
938    /// Byte range of the instruction data within the original buffer.
939    pub instruction_data_range: core::ops::Range<usize>,
940    /// Byte offset of the 32-byte program id within the original buffer.
941    pub program_id_offset: usize,
942    /// Byte offsets of each account slot, indexable 0..account_count.
943    pub slot_offsets: [usize; MAX_SAFE_ACCOUNT_SLOTS],
944}
945
946/// Errors returned by the safe parser.
947#[derive(Clone, Copy, Debug, PartialEq, Eq)]
948pub enum FrameError {
949    /// Buffer ended before the full frame could be parsed.
950    UnexpectedEof { needed: usize, at: usize },
951    /// Account count exceeds the compiled-in cap (256).
952    AccountCountOutOfRange(u64),
953    /// Duplicate marker refers to a non-earlier slot (forward ref or self).
954    MalformedDuplicateMarker { slot: usize, marker: u8 },
955    /// Data length field larger than the remaining buffer.
956    DataLenOutOfRange { slot: usize, data_len: u64 },
957    /// Arithmetic overflow while computing the next slot offset.
958    OffsetOverflow { slot: usize },
959}
960
961impl core::fmt::Display for FrameError {
962    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
963        match self {
964            Self::UnexpectedEof { needed, at } => {
965                write!(f, "unexpected EOF: need {needed} bytes at offset {at}")
966            }
967            Self::AccountCountOutOfRange(n) => {
968                write!(f, "account count {n} exceeds cap 256")
969            }
970            Self::MalformedDuplicateMarker { slot, marker } => {
971                write!(
972                    f,
973                    "malformed duplicate marker at slot {slot}: marker {marker} does not refer to an earlier slot"
974                )
975            }
976            Self::DataLenOutOfRange { slot, data_len } => {
977                write!(
978                    f,
979                    "slot {slot}: data_len {data_len} exceeds remaining buffer"
980                )
981            }
982            Self::OffsetOverflow { slot } => {
983                write!(f, "slot {slot}: offset arithmetic overflow")
984            }
985        }
986    }
987}
988
989/// Parse a loader-input byte buffer with full bounds checking.
990///
991/// This is the safe companion to `scan_instruction_frame` /
992/// `deserialize_accounts`. It returns `Err` (never panics, never reads
993/// out of bounds) for any malformed or truncated input, and preserves
994/// the exact same forward-duplicate-marker rejection rule that the
995/// pointer parser uses (see `malformed_duplicate_marker`).
996///
997/// Off-chain tools, fuzz harnesses, and RPC decoders should prefer
998/// this function. On-chain entrypoints continue to use the pointer
999/// parser for zero-overhead access.
1000pub fn parse_instruction_frame_checked(buf: &[u8]) -> Result<FrameInfo, FrameError> {
1001    // Helper: read a u64 LE at `pos`, bumping the cursor. Returns
1002    // `UnexpectedEof` if the 8 bytes aren't in range.
1003    fn read_u64_le(buf: &[u8], pos: &mut usize) -> Result<u64, FrameError> {
1004        let end = pos
1005            .checked_add(8)
1006            .ok_or(FrameError::OffsetOverflow { slot: 0 })?;
1007        let slice = buf.get(*pos..end).ok_or(FrameError::UnexpectedEof {
1008            needed: 8,
1009            at: *pos,
1010        })?;
1011        let mut bytes = [0u8; 8];
1012        bytes.copy_from_slice(slice);
1013        *pos = end;
1014        Ok(u64::from_le_bytes(bytes))
1015    }
1016
1017    fn read_u8(buf: &[u8], pos: &mut usize) -> Result<u8, FrameError> {
1018        let byte = *buf.get(*pos).ok_or(FrameError::UnexpectedEof {
1019            needed: 1,
1020            at: *pos,
1021        })?;
1022        *pos += 1;
1023        Ok(byte)
1024    }
1025
1026    fn advance(buf: &[u8], pos: &mut usize, n: usize) -> Result<(), FrameError> {
1027        let end = pos
1028            .checked_add(n)
1029            .ok_or(FrameError::OffsetOverflow { slot: 0 })?;
1030        if end > buf.len() {
1031            return Err(FrameError::UnexpectedEof {
1032                needed: n,
1033                at: *pos,
1034            });
1035        }
1036        *pos = end;
1037        Ok(())
1038    }
1039
1040    let mut pos = 0usize;
1041    let account_count = read_u64_le(buf, &mut pos)?;
1042    if account_count > MAX_SAFE_ACCOUNT_SLOTS as u64 {
1043        return Err(FrameError::AccountCountOutOfRange(account_count));
1044    }
1045    let account_count = account_count as usize;
1046
1047    let mut slot_offsets = [0usize; MAX_SAFE_ACCOUNT_SLOTS];
1048
1049    // The slot index is load-bearing: it backs the duplicate-marker invariant
1050    // (`duplicate_of >= slot`) and every `FrameError { slot, .. }` report, so
1051    // an iterator over values would lose the information the loop exists for.
1052    #[allow(clippy::needless_range_loop)]
1053    for slot in 0..account_count {
1054        let slot_start = pos;
1055        slot_offsets[slot] = slot_start;
1056
1057        let marker = read_u8(buf, &mut pos)?;
1058        if marker == u8::MAX {
1059            // Canonical account: the remaining 87 bytes of RuntimeAccount
1060            // follow (we already consumed the marker byte).
1061            advance(buf, &mut pos, RuntimeAccount::SIZE - 1).map_err(|_| {
1062                FrameError::UnexpectedEof {
1063                    needed: RuntimeAccount::SIZE - 1,
1064                    at: pos,
1065                }
1066            })?;
1067            // data_len lives at offset 80 in RuntimeAccount; we read it
1068            // directly from the slot header. Offset within this slot:
1069            // borrow_state(1) + flags(3) + resize_delta(4) + address(32) +
1070            // owner(32) + lamports(8) = 80 -> data_len(8).
1071            let data_len_pos = slot_start
1072                .checked_add(80)
1073                .ok_or(FrameError::OffsetOverflow { slot })?;
1074            let mut dl_bytes = [0u8; 8];
1075            let dl_slice =
1076                buf.get(data_len_pos..data_len_pos + 8)
1077                    .ok_or(FrameError::UnexpectedEof {
1078                        needed: 8,
1079                        at: data_len_pos,
1080                    })?;
1081            dl_bytes.copy_from_slice(dl_slice);
1082            let data_len = u64::from_le_bytes(dl_bytes);
1083
1084            // data_bytes + realloc reserve + u128 alignment padding + rent_epoch
1085            let data_sz: usize = (data_len as usize)
1086                .checked_add(MAX_PERMITTED_DATA_INCREASE)
1087                .ok_or(FrameError::DataLenOutOfRange { slot, data_len })?;
1088            advance(buf, &mut pos, data_sz)
1089                .map_err(|_| FrameError::DataLenOutOfRange { slot, data_len })?;
1090            let pad = pos.wrapping_neg() & (BPF_ALIGN_OF_U128 - 1);
1091            advance(buf, &mut pos, pad).map_err(|_| FrameError::UnexpectedEof {
1092                needed: pad,
1093                at: pos,
1094            })?;
1095            advance(buf, &mut pos, 8)
1096                .map_err(|_| FrameError::UnexpectedEof { needed: 8, at: pos })?;
1097        } else {
1098            // Duplicate marker: must refer to a strictly earlier slot.
1099            // Duplicate markers may only refer to a previously parsed slot.
1100            let duplicate_of = marker as usize;
1101            if duplicate_of >= slot {
1102                return Err(FrameError::MalformedDuplicateMarker { slot, marker });
1103            }
1104            // 7 padding bytes follow the marker.
1105            advance(buf, &mut pos, 7)
1106                .map_err(|_| FrameError::UnexpectedEof { needed: 7, at: pos })?;
1107        }
1108    }
1109
1110    // Instruction data: u64 LE length prefix + bytes.
1111    let ix_data_len = read_u64_le(buf, &mut pos)? as usize;
1112    let ix_start = pos;
1113    advance(buf, &mut pos, ix_data_len).map_err(|_| FrameError::UnexpectedEof {
1114        needed: ix_data_len,
1115        at: pos,
1116    })?;
1117    let instruction_data_range = ix_start..pos;
1118
1119    // 32-byte program id trailer.
1120    let program_id_offset = pos;
1121    advance(buf, &mut pos, 32).map_err(|_| FrameError::UnexpectedEof {
1122        needed: 32,
1123        at: pos,
1124    })?;
1125
1126    Ok(FrameInfo {
1127        account_count,
1128        instruction_data_range,
1129        program_id_offset,
1130        slot_offsets,
1131    })
1132}
1133
1134#[cfg(test)]
1135mod checked_parser_tests {
1136    use super::*;
1137
1138    /// Size of the single-account canonical frame used by tests.
1139    /// 8 (account_count) + 88 (RuntimeAccount) + 10240 (realloc reserve)
1140    /// + 0 (already u128-aligned at 10336) + 8 (rent_epoch)
1141    /// + 8 (ix_data_len) + 32 (program_id) = 10384
1142    const MINIMAL_FRAME_LEN: usize = 8 + 88 + MAX_PERMITTED_DATA_INCREASE + 8 + 8 + 32;
1143
1144    /// Build a valid one-canonical-account frame with zero-byte data.
1145    fn build_minimal_frame() -> [u8; MINIMAL_FRAME_LEN] {
1146        let mut buf = [0u8; MINIMAL_FRAME_LEN];
1147        buf[0..8].copy_from_slice(&1u64.to_le_bytes()); // account_count = 1
1148        buf[8] = 0xFF; // marker = canonical
1149                       // remaining bytes of RuntimeAccount stay zero
1150                       // realloc reserve stays zero
1151                       // rent_epoch zero
1152                       // ix_data_len = 0 (already zero)
1153                       // program_id stays zero
1154        buf
1155    }
1156
1157    #[test]
1158    fn parses_minimal_valid_frame() {
1159        let buf = build_minimal_frame();
1160        let frame = parse_instruction_frame_checked(&buf).expect("well-formed");
1161        assert_eq!(frame.account_count, 1);
1162        assert_eq!(frame.instruction_data_range.len(), 0);
1163        assert_eq!(frame.program_id_offset + 32, buf.len());
1164    }
1165
1166    #[test]
1167    fn truncated_header_is_rejected() {
1168        let buf = [0u8; 4]; // less than 8 bytes = no room for account_count
1169        let err = parse_instruction_frame_checked(&buf).unwrap_err();
1170        assert!(matches!(err, FrameError::UnexpectedEof { .. }));
1171    }
1172
1173    #[test]
1174    fn oversized_account_count_is_rejected() {
1175        let mut buf = [0u8; 8];
1176        buf.copy_from_slice(&1_000u64.to_le_bytes());
1177        let err = parse_instruction_frame_checked(&buf).unwrap_err();
1178        assert!(matches!(err, FrameError::AccountCountOutOfRange(1000)));
1179    }
1180
1181    #[test]
1182    fn forward_duplicate_marker_is_rejected() {
1183        // 2-account frame where slot 0 is a duplicate of slot 1
1184        // (forward reference). Must be rejected.
1185        let mut buf = [0u8; 16];
1186        buf[0..8].copy_from_slice(&2u64.to_le_bytes());
1187        buf[8] = 1; // slot 0 marker = 1 (forward ref)
1188        let err = parse_instruction_frame_checked(&buf).unwrap_err();
1189        assert!(matches!(
1190            err,
1191            FrameError::MalformedDuplicateMarker { slot: 0, marker: 1 }
1192        ));
1193    }
1194
1195    #[test]
1196    fn self_duplicate_marker_is_rejected() {
1197        // Slot 0 marker=0 is self-reference: forbidden.
1198        let mut buf = [0u8; 16];
1199        buf[0..8].copy_from_slice(&1u64.to_le_bytes());
1200        buf[8] = 0; // marker = 0, referring to slot 0 itself
1201        let err = parse_instruction_frame_checked(&buf).unwrap_err();
1202        assert!(matches!(
1203            err,
1204            FrameError::MalformedDuplicateMarker { slot: 0, marker: 0 }
1205        ));
1206    }
1207
1208    #[test]
1209    fn arbitrary_short_input_never_panics() {
1210        // Bounds-checking contract: feeding every length from 0..=256
1211        // bytes of zeroes must never panic or UB.
1212        let buf = [0u8; 256];
1213        for len in 0..=256 {
1214            let _ = parse_instruction_frame_checked(&buf[..len]);
1215        }
1216    }
1217
1218    #[test]
1219    fn arbitrary_ff_input_never_panics() {
1220        let buf = [0xFFu8; 256];
1221        for len in 0..=256 {
1222            let _ = parse_instruction_frame_checked(&buf[..len]);
1223        }
1224    }
1225}
1226
1227#[cfg(test)]
1228mod fused_walk_tests {
1229    extern crate std;
1230
1231    use std::vec;
1232    use std::vec::Vec;
1233
1234    use super::*;
1235
1236    /// One account slot description for the frame builder.
1237    enum Slot {
1238        /// Canonical account: 0xFF marker, header, `data` bytes, realloc
1239        /// reserve, alignment padding, rent epoch.
1240        Fresh { data: Vec<u8>, lamports: u64 },
1241        /// Duplicate reference: 1 marker byte + 7 padding bytes.
1242        Dup(u8),
1243    }
1244
1245    fn fresh(data_len: usize, lamports: u64) -> Slot {
1246        Slot::Fresh {
1247            data: vec![0xABu8; data_len],
1248            lamports,
1249        }
1250    }
1251
1252    /// 8-aligned loader-input fixture. The `u64` backing guarantees the
1253    /// base pointer is 8-aligned, matching the loader's `MM_INPUT_START`
1254    /// guarantee that the fused stride math relies on.
1255    struct Frame {
1256        words: Vec<u64>,
1257    }
1258
1259    impl Frame {
1260        fn as_mut_ptr(&mut self) -> *mut u8 {
1261            self.words.as_mut_ptr() as *mut u8
1262        }
1263    }
1264
1265    /// Serialize a loader input frame exactly per the Solana BPF loader
1266    /// layout: u64 account count; per canonical account an 88-byte
1267    /// `RuntimeAccount` header (marker byte 0xFF first), `data_len` data
1268    /// bytes, `MAX_PERMITTED_DATA_INCREASE` reserve, padding to the next
1269    /// 8-byte boundary, and an 8-byte rent epoch; per duplicate 8 bytes
1270    /// (marker + 7 padding); then u64 ix-data length, ix-data bytes, and
1271    /// the 32-byte program id.
1272    fn build_frame(slots: &[Slot], ix_data: &[u8], program_id: [u8; 32]) -> Frame {
1273        let mut buf: Vec<u8> = Vec::new();
1274        buf.extend_from_slice(&(slots.len() as u64).to_le_bytes());
1275
1276        for (i, slot) in slots.iter().enumerate() {
1277            match slot {
1278                Slot::Fresh { data, lamports } => {
1279                    let mut header = [0u8; RuntimeAccount::SIZE];
1280                    header[0] = 0xFF; // canonical marker / borrow_state
1281                    header[1] = 1; // is_signer
1282                    header[2] = 1; // is_writable
1283                                   // address: recognizable per-slot pattern
1284                    header[8..40].copy_from_slice(&[i as u8 + 1; 32]);
1285                    // owner
1286                    header[40..72].copy_from_slice(&[0x55; 32]);
1287                    // lamports at offset 72
1288                    header[72..80].copy_from_slice(&lamports.to_le_bytes());
1289                    // data_len at offset 80
1290                    header[80..88].copy_from_slice(&(data.len() as u64).to_le_bytes());
1291                    buf.extend_from_slice(&header);
1292                    buf.extend_from_slice(data);
1293                    buf.extend_from_slice(&vec![0u8; MAX_PERMITTED_DATA_INCREASE]);
1294                    // Pad to the next 8-byte boundary. The base is 8-aligned,
1295                    // so padding the relative length equals padding the
1296                    // absolute address; this is the loader's ground truth.
1297                    while !buf.len().is_multiple_of(BPF_ALIGN_OF_U128) {
1298                        buf.push(0);
1299                    }
1300                    // rent epoch
1301                    buf.extend_from_slice(&u64::MAX.to_le_bytes());
1302                }
1303                Slot::Dup(of) => {
1304                    buf.push(*of);
1305                    buf.extend_from_slice(&[0u8; 7]);
1306                }
1307            }
1308        }
1309
1310        buf.extend_from_slice(&(ix_data.len() as u64).to_le_bytes());
1311        buf.extend_from_slice(ix_data);
1312        buf.extend_from_slice(&program_id);
1313
1314        // Copy into 8-aligned u64 backing.
1315        let mut words = vec![0u64; buf.len().div_ceil(8)];
1316        // SAFETY: `words` has at least `buf.len()` bytes of capacity and the
1317        // regions do not overlap.
1318        unsafe {
1319            core::ptr::copy_nonoverlapping(buf.as_ptr(), words.as_mut_ptr() as *mut u8, buf.len());
1320        }
1321        Frame { words }
1322    }
1323
1324    fn uninit_views<'a, const MAX: usize>() -> [MaybeUninit<AccountView<'a>>; MAX] {
1325        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
1326        // state by definition.
1327        unsafe { MaybeUninit::uninit().assume_init() }
1328    }
1329
1330    const PID: [u8; 32] = [0xC4; 32];
1331
1332    #[test]
1333    fn zero_accounts_finds_ix_data_and_program_id() {
1334        let mut frame = build_frame(&[], &[9, 8, 7], PID);
1335        let mut views = uninit_views::<4>();
1336        // SAFETY: `frame` is a well-formed loader-layout buffer with an
1337        // 8-aligned base.
1338        let (pid, count, ix) = unsafe { deserialize_accounts::<4>(frame.as_mut_ptr(), &mut views) };
1339        assert_eq!(count, 0);
1340        assert_eq!(ix, &[9, 8, 7]);
1341        assert_eq!(pid.as_array(), &PID);
1342    }
1343
1344    #[test]
1345    fn one_account_materializes_and_finds_tail() {
1346        let mut frame = build_frame(&[fresh(11, 42)], &[1, 2, 3, 4], PID);
1347        let mut views = uninit_views::<4>();
1348        // SAFETY: well-formed 8-aligned loader-layout fixture.
1349        let (pid, count, ix) = unsafe { deserialize_accounts::<4>(frame.as_mut_ptr(), &mut views) };
1350        assert_eq!(count, 1);
1351        // SAFETY: slot 0 was initialized by the parser (count == 1).
1352        let view = unsafe { views[0].assume_init_ref() };
1353        assert_eq!(view.data_len(), 11);
1354        assert_eq!(view.lamports(), 42);
1355        assert!(view.is_signer());
1356        assert_eq!(ix, &[1, 2, 3, 4]);
1357        assert_eq!(pid.as_array(), &PID);
1358    }
1359
1360    #[test]
1361    fn leading_prefix_materializes_only_the_declared_accounts() {
1362        // Five records, a duplicate of slot 0 among them; only the first
1363        // three are asked for, and the walk never has to reach the tail.
1364        let slots = [
1365            fresh(9, 7),
1366            Slot::Dup(0),
1367            fresh(3, 8),
1368            fresh(5, 9),
1369            fresh(1, 10),
1370        ];
1371        let mut frame = build_frame(&slots, &[0x11], PID);
1372        let mut views = uninit_views::<3>();
1373        // SAFETY: well-formed 8-aligned loader-layout fixture.
1374        let count = unsafe { deserialize_leading_accounts::<3>(frame.as_mut_ptr(), &mut views, 3) };
1375        assert_eq!(count, 3);
1376        // SAFETY: the first three slots were initialized (count == 3).
1377        let (a, b, c) = unsafe {
1378            (
1379                views[0].assume_init_ref(),
1380                views[1].assume_init_ref(),
1381                views[2].assume_init_ref(),
1382            )
1383        };
1384        assert_eq!(a.data_len(), 9);
1385        assert_eq!(b.raw_ptr(), a.raw_ptr(), "the duplicate aliases slot 0");
1386        assert_eq!(c.data_len(), 3);
1387        assert_eq!(c.lamports(), 8);
1388
1389        // Fewer accounts than the bound: the count is the loader's, and
1390        // the caller's binder decides whether that is enough.
1391        let mut frame = build_frame(&[fresh(2, 1)], &[0x11], PID);
1392        let mut views = uninit_views::<3>();
1393        // SAFETY: well-formed 8-aligned loader-layout fixture.
1394        let count = unsafe { deserialize_leading_accounts::<3>(frame.as_mut_ptr(), &mut views, 3) };
1395        assert_eq!(count, 1);
1396
1397        // A narrower arm bound inside the same scratch: only that many.
1398        let mut frame = build_frame(&slots, &[0x11], PID);
1399        let mut views = uninit_views::<3>();
1400        // SAFETY: well-formed 8-aligned loader-layout fixture.
1401        let count = unsafe { deserialize_leading_accounts::<3>(frame.as_mut_ptr(), &mut views, 2) };
1402        assert_eq!(count, 2);
1403    }
1404
1405    #[test]
1406    fn exactly_max_accounts() {
1407        let slots: Vec<Slot> = (0..4).map(|i| fresh(i * 3 + 1, 100 + i as u64)).collect();
1408        let mut frame = build_frame(&slots, &[0xEE; 5], PID);
1409        let mut views = uninit_views::<4>();
1410        // SAFETY: well-formed 8-aligned loader-layout fixture.
1411        let (pid, count, ix) = unsafe { deserialize_accounts::<4>(frame.as_mut_ptr(), &mut views) };
1412        assert_eq!(count, 4);
1413        for (i, view) in views.iter().enumerate() {
1414            // SAFETY: slots 0..count were initialized by the parser.
1415            let view = unsafe { view.assume_init_ref() };
1416            assert_eq!(view.data_len(), i * 3 + 1);
1417            assert_eq!(view.lamports(), 100 + i as u64);
1418        }
1419        assert_eq!(ix, &[0xEE; 5]);
1420        assert_eq!(pid.as_array(), &PID);
1421    }
1422
1423    #[test]
1424    fn beyond_max_is_skip_only_and_tail_still_found() {
1425        // MAX = 4, 7 accounts (MAX + 3). The tail accounts get assorted
1426        // data_len residues so the skip-only stride is exercised too.
1427        let slots: Vec<Slot> = (0..7).map(|i| fresh(i * 5 + 2, i as u64)).collect();
1428        let mut frame = build_frame(&slots, &[0xD1, 0xD2], PID);
1429        let mut views = uninit_views::<4>();
1430        // SAFETY: well-formed 8-aligned loader-layout fixture.
1431        let (pid, count, ix) = unsafe { deserialize_accounts::<4>(frame.as_mut_ptr(), &mut views) };
1432        assert_eq!(count, 4);
1433        for (i, view) in views.iter().enumerate() {
1434            // SAFETY: slots 0..count were initialized by the parser.
1435            let view = unsafe { view.assume_init_ref() };
1436            assert_eq!(view.data_len(), i * 5 + 2);
1437        }
1438        assert_eq!(ix, &[0xD1, 0xD2]);
1439        assert_eq!(pid.as_array(), &PID);
1440    }
1441
1442    #[test]
1443    fn duplicates_alias_the_canonical_record() {
1444        let slots = [fresh(9, 7), Slot::Dup(0), fresh(3, 8), Slot::Dup(2)];
1445        let mut frame = build_frame(&slots, &[0x11], PID);
1446        let mut views = uninit_views::<8>();
1447        // SAFETY: well-formed 8-aligned loader-layout fixture.
1448        let (_, count, ix) = unsafe { deserialize_accounts::<8>(frame.as_mut_ptr(), &mut views) };
1449        assert_eq!(count, 4);
1450        // SAFETY: slots 0..count were initialized by the parser.
1451        let (v0, v1, v2, v3) = unsafe {
1452            (
1453                views[0].assume_init_ref(),
1454                views[1].assume_init_ref(),
1455                views[2].assume_init_ref(),
1456                views[3].assume_init_ref(),
1457            )
1458        };
1459        assert_eq!(v0.raw_ptr(), v1.raw_ptr(), "dup slot aliases canonical");
1460        assert_eq!(v2.raw_ptr(), v3.raw_ptr(), "dup slot aliases canonical");
1461        assert_ne!(v0.raw_ptr(), v2.raw_ptr());
1462        assert_eq!(v1.data_len(), 9);
1463        assert_eq!(v3.data_len(), 3);
1464        assert_eq!(ix, &[0x11]);
1465    }
1466
1467    #[test]
1468    fn duplicate_in_skip_only_tail_advances_eight_bytes() {
1469        // MAX = 2; slots 2 and 3 (a fresh account and a duplicate) are
1470        // skip-only. If the duplicate stride were wrong, the ix data would
1471        // be misread.
1472        let slots = [fresh(5, 1), fresh(6, 2), fresh(7, 3), Slot::Dup(1)];
1473        let mut frame = build_frame(&slots, &[0xAA, 0xBB, 0xCC], PID);
1474        let mut views = uninit_views::<2>();
1475        // SAFETY: well-formed 8-aligned loader-layout fixture.
1476        let (pid, count, ix) = unsafe { deserialize_accounts::<2>(frame.as_mut_ptr(), &mut views) };
1477        assert_eq!(count, 2);
1478        assert_eq!(ix, &[0xAA, 0xBB, 0xCC]);
1479        assert_eq!(pid.as_array(), &PID);
1480    }
1481
1482    #[test]
1483    fn every_data_len_alignment_residue_walks_correctly() {
1484        // data_len 0..=7 covers every alignment residue; 8..=15 repeats them
1485        // one stride later. All must land the cursor exactly on the ix tail.
1486        for base in [0usize, 8] {
1487            let slots: Vec<Slot> = (0..8).map(|r| fresh(base + r, r as u64)).collect();
1488            let mut frame = build_frame(&slots, &[0x42; 9], PID);
1489            let mut views = uninit_views::<8>();
1490            // SAFETY: well-formed 8-aligned loader-layout fixture.
1491            let (pid, count, ix) =
1492                unsafe { deserialize_accounts::<8>(frame.as_mut_ptr(), &mut views) };
1493            assert_eq!(count, 8);
1494            for (r, view) in views.iter().enumerate() {
1495                // SAFETY: slots 0..count were initialized by the parser.
1496                let view = unsafe { view.assume_init_ref() };
1497                assert_eq!(view.data_len(), base + r);
1498            }
1499            assert_eq!(ix, &[0x42; 9]);
1500            assert_eq!(pid.as_array(), &PID);
1501        }
1502    }
1503
1504    /// Differential test: the folded integer stride must match the old
1505    /// pointer `align_offset` formula byte-for-byte for every data_len,
1506    /// given an 8-aligned base (the loader guarantee).
1507    #[test]
1508    fn folded_stride_matches_align_offset_formula() {
1509        // Real 8-aligned base pointer; align_offset is pure address
1510        // arithmetic, so wrapping_add beyond the allocation is fine.
1511        let backing = [0u64; 1];
1512        let base = backing.as_ptr() as *const u8;
1513        assert_eq!(base as usize % 8, 0, "test base must be 8-aligned");
1514
1515        for start in [8usize, 96, 10344, 20696] {
1516            for data_len in 0usize..64 {
1517                // Old formula (pre-fusion deserialize_accounts body):
1518                let mut old = start;
1519                old += RuntimeAccount::SIZE;
1520                old += data_len + MAX_PERMITTED_DATA_INCREASE;
1521                old += base.wrapping_add(old).align_offset(BPF_ALIGN_OF_U128);
1522                old += 8;
1523                // New folded formula:
1524                let new = next_record_offset(start, data_len);
1525                assert_eq!(
1526                    old, new,
1527                    "stride mismatch at start={start} data_len={data_len}"
1528                );
1529            }
1530        }
1531    }
1532
1533    #[test]
1534    fn next_record_matches_the_offset_stride() {
1535        // `next_record` is the offset stride applied to an 8-aligned
1536        // pointer; for every data-length residue both land on one byte.
1537        let mut backing = vec![0u64; 5_000];
1538        let base = backing.as_mut_ptr() as *mut u8;
1539        for start in [8usize, 96, 10344, 20696] {
1540            for data_len in 0usize..64 {
1541                // SAFETY: `start` plus one record's whole stride stays inside
1542                // the 40,000-byte backing.
1543                let got = unsafe { next_record(base.add(start), data_len) };
1544                assert_eq!(
1545                    got as usize - base as usize,
1546                    next_record_offset(start, data_len),
1547                    "start={start} data_len={data_len}"
1548                );
1549            }
1550        }
1551    }
1552
1553    #[test]
1554    fn take_canonical_writes_the_view_records_the_baseline_and_strides() {
1555        let mut frame = build_frame(&[fresh(13, 7), fresh(2, 1)], &[], PID);
1556        let base = frame.as_mut_ptr();
1557        let mut views = uninit_views::<1>();
1558        let slots = views.as_mut_ptr() as *mut AccountView<'_>;
1559        // SAFETY: slot 0's record starts after the 8-byte count, and `views`
1560        // has one writable slot.
1561        let next = unsafe { take_canonical(base.add(8), slots) };
1562        // SAFETY: `take_canonical` wrote slot 0.
1563        let view = unsafe { views[0].assume_init_ref() };
1564        assert_eq!(view.raw_ptr() as usize, base as usize + 8);
1565        assert_eq!(view.original_data_len(), 13);
1566        assert_eq!(next as usize - base as usize, next_record_offset(8, 13));
1567    }
1568
1569    #[test]
1570    fn take_duplicate_aliases_an_earlier_slot_and_crosses_eight_bytes() {
1571        let mut frame = build_frame(&[fresh(4, 1), Slot::Dup(0)], &[], PID);
1572        let base = frame.as_mut_ptr();
1573        let mut views = uninit_views::<2>();
1574        let slots = views.as_mut_ptr() as *mut AccountView<'_>;
1575        // SAFETY: slot 0's record starts after the count; `views` has room.
1576        let duplicate = unsafe { take_canonical(base.add(8), slots) };
1577        // SAFETY: slot 0 is initialized, slot 1 is writable, and `duplicate`
1578        // is slot 1's record.
1579        let next = unsafe { take_duplicate(duplicate, 0, 1, slots) };
1580        // SAFETY: both slots were written above.
1581        let (first, second) = unsafe { (views[0].assume_init_ref(), views[1].assume_init_ref()) };
1582        assert_eq!(first.raw_ptr(), second.raw_ptr());
1583        assert_eq!(next as usize, duplicate as usize + 8);
1584    }
1585
1586    #[test]
1587    #[should_panic(expected = "malformed duplicate marker")]
1588    fn take_duplicate_traps_on_a_marker_that_is_not_earlier() {
1589        let mut frame = build_frame(&[fresh(4, 1), Slot::Dup(1)], &[], PID);
1590        let base = frame.as_mut_ptr();
1591        let mut views = uninit_views::<2>();
1592        let slots = views.as_mut_ptr() as *mut AccountView<'_>;
1593        // SAFETY: slot 0's record starts after the count; `views` has room.
1594        let duplicate = unsafe { take_canonical(base.add(8), slots) };
1595        // SAFETY: the self-reference is the condition under test; the trap
1596        // fires before any slot is read.
1597        let _ = unsafe { take_duplicate(duplicate, 1, 1, slots) };
1598    }
1599
1600    #[test]
1601    fn take_record_follows_the_marker() {
1602        let mut frame = build_frame(&[fresh(0, 1), Slot::Dup(0), fresh(9, 2)], &[3], PID);
1603        let base = frame.as_mut_ptr();
1604        let mut views = uninit_views::<3>();
1605        let slots = views.as_mut_ptr() as *mut AccountView<'_>;
1606        // SAFETY: the records start after the count.
1607        let mut cursor = unsafe { base.add(8) };
1608        for slot in 0..3 {
1609            // SAFETY: `cursor` is slot `slot`'s record, `slot < 3`, and the
1610            // earlier slots were written by earlier iterations.
1611            cursor = unsafe { take_record(cursor, slot, slots) };
1612        }
1613        // SAFETY: all three slots were written.
1614        let (a, b, c) = unsafe {
1615            (
1616                views[0].assume_init_ref(),
1617                views[1].assume_init_ref(),
1618                views[2].assume_init_ref(),
1619            )
1620        };
1621        assert_eq!(a.raw_ptr(), b.raw_ptr());
1622        assert_ne!(a.raw_ptr(), c.raw_ptr());
1623        assert_eq!(c.original_data_len(), 9);
1624        // The cursor ends on the instruction-data length word.
1625        // SAFETY: the walk crossed every record, and the tail follows.
1626        assert_eq!(
1627            unsafe { core::ptr::read_unaligned(cursor as *const u64) },
1628            1
1629        );
1630    }
1631
1632    #[test]
1633    fn materialize_stops_at_the_count_and_at_the_bound() {
1634        let slots = [
1635            fresh(1, 1),
1636            Slot::Dup(0),
1637            fresh(3, 3),
1638            fresh(4, 4),
1639            fresh(5, 5),
1640            Slot::Dup(2),
1641        ];
1642        let mut frame = build_frame(&slots, &[], PID);
1643        let base = frame.as_mut_ptr();
1644
1645        // The count comes first: two of six.
1646        let mut views = uninit_views::<8>();
1647        // SAFETY: the records start after the count, and 2 <= 6.
1648        let (count, _) = unsafe { materialize::<8>(base.add(8), 2, &mut views) };
1649        assert_eq!(count, 2);
1650
1651        // The bound comes first: the unrolled prefix returns at three.
1652        let mut views = uninit_views::<3>();
1653        // SAFETY: as above; six is the loader's count.
1654        let (count, cursor) = unsafe { materialize::<3>(base.add(8), 6, &mut views) };
1655        assert_eq!(count, 3);
1656        // SAFETY: the cursor sits on the fourth record's marker.
1657        assert_eq!(unsafe { *cursor }, u8::MAX);
1658
1659        // All six: a duplicate in the unrolled prefix hands over to the loop,
1660        // which also takes the slots past four.
1661        let mut views = uninit_views::<8>();
1662        // SAFETY: as above.
1663        let (count, cursor) = unsafe { materialize::<8>(base.add(8), 6, &mut views) };
1664        assert_eq!(count, 6);
1665        // SAFETY: all six slots were written.
1666        let (v0, v1, v2, v5) = unsafe {
1667            (
1668                views[0].assume_init_ref(),
1669                views[1].assume_init_ref(),
1670                views[2].assume_init_ref(),
1671                views[5].assume_init_ref(),
1672            )
1673        };
1674        assert_eq!(v0.raw_ptr(), v1.raw_ptr());
1675        assert_eq!(v2.raw_ptr(), v5.raw_ptr());
1676        // SAFETY: the walk crossed every record; the length word follows.
1677        assert_eq!(
1678            unsafe { core::ptr::read_unaligned(cursor as *const u64) },
1679            0
1680        );
1681    }
1682
1683    #[test]
1684    fn huge_data_len_near_region_end() {
1685        // A single account whose data dwarfs the rest of the frame; the
1686        // ix tail sits immediately after its (padded) record.
1687        let big = 100_003usize; // residue 3 to force nonzero padding
1688        let mut frame = build_frame(&[fresh(big, 5)], &[0x77, 0x66], PID);
1689        let mut views = uninit_views::<2>();
1690        // SAFETY: well-formed 8-aligned loader-layout fixture.
1691        let (pid, count, ix) = unsafe { deserialize_accounts::<2>(frame.as_mut_ptr(), &mut views) };
1692        assert_eq!(count, 1);
1693        // SAFETY: slot 0 was initialized by the parser.
1694        assert_eq!(unsafe { views[0].assume_init_ref() }.data_len(), big);
1695        assert_eq!(ix, &[0x77, 0x66]);
1696        assert_eq!(pid.as_array(), &PID);
1697    }
1698
1699    #[test]
1700    fn account_count_clamps_at_254_materialized_slots() {
1701        // 1 canonical + 259 duplicates = 260 declared accounts. Slots
1702        // 254..259 must be skip-only even though MAX = 255, mirroring the
1703        // pre-fusion `min(254)` clamp; the walk must still reach the tail.
1704        let mut slots: Vec<Slot> = vec![fresh(4, 9)];
1705        slots.extend((0..259).map(|_| Slot::Dup(0)));
1706        let mut frame = build_frame(&slots, &[0x0F; 3], PID);
1707        let mut views = uninit_views::<255>();
1708        // SAFETY: well-formed 8-aligned loader-layout fixture.
1709        let (pid, count, ix) =
1710            unsafe { deserialize_accounts::<255>(frame.as_mut_ptr(), &mut views) };
1711        assert_eq!(count, 254);
1712        assert_eq!(ix, &[0x0F; 3]);
1713        assert_eq!(pid.as_array(), &PID);
1714    }
1715
1716    #[test]
1717    #[should_panic(expected = "malformed duplicate marker")]
1718    fn forward_duplicate_marker_traps_in_materialize_range() {
1719        let slots = [fresh(1, 1), Slot::Dup(1)]; // self-reference at slot 1
1720        let mut frame = build_frame(&slots, &[], PID);
1721        let mut views = uninit_views::<4>();
1722        // SAFETY: buffer layout is loader-shaped; the malformed marker is
1723        // the condition under test and traps before any OOB access.
1724        let _ = unsafe { deserialize_accounts::<4>(frame.as_mut_ptr(), &mut views) };
1725    }
1726
1727    #[test]
1728    fn forward_duplicate_marker_in_skip_only_tail_is_crossed_not_viewed() {
1729        // MAX = 1, so slot 1 is skip-only. No view is made from it, so a
1730        // marker naming a later slot cannot alias anything: the walk crosses
1731        // its 8 bytes and still finds the instruction tail exactly.
1732        let slots = [fresh(1, 1), Slot::Dup(5)];
1733        let mut frame = build_frame(&slots, &[7, 8], PID);
1734        let mut views = uninit_views::<1>();
1735        // SAFETY: buffer layout is loader-shaped.
1736        let (pid, count, ix) = unsafe { deserialize_accounts::<1>(frame.as_mut_ptr(), &mut views) };
1737        assert_eq!(count, 1);
1738        assert_eq!(ix, &[7, 8]);
1739        assert_eq!(pid.as_array(), &PID);
1740    }
1741
1742    #[test]
1743    fn fast_variant_uses_same_stride_and_aliases_duplicates() {
1744        // `deserialize_accounts_fast` shares `next_record_offset`; verify it
1745        // still parses mixed-residue accounts and duplicates correctly when
1746        // ix data and program id are supplied out of band.
1747        let slots = [fresh(13, 3), Slot::Dup(0), fresh(6, 4)];
1748        let mut frame = build_frame(&slots, &[0x99], PID);
1749        let mut views = uninit_views::<4>();
1750        let ix: &[u8] = &[0x99];
1751        let program_id = Address::new_from_array(PID);
1752        // SAFETY: well-formed 8-aligned loader-layout fixture; ix data and
1753        // program id are supplied directly per the fast-path contract.
1754        let (pid, count, out_ix) = unsafe {
1755            deserialize_accounts_fast::<4>(frame.as_mut_ptr(), &mut views, ix, &program_id)
1756        };
1757        assert_eq!(count, 3);
1758        // SAFETY: slots 0..count were initialized by the parser.
1759        let (v0, v1, v2) = unsafe {
1760            (
1761                views[0].assume_init_ref(),
1762                views[1].assume_init_ref(),
1763                views[2].assume_init_ref(),
1764            )
1765        };
1766        assert_eq!(v0.raw_ptr(), v1.raw_ptr());
1767        assert_eq!(v0.data_len(), 13);
1768        assert_eq!(v2.data_len(), 6);
1769        assert_eq!(out_ix, ix);
1770        assert_eq!(pid.as_array(), &PID);
1771    }
1772
1773    /// The fused walk and the safe checked parser must agree on where the
1774    /// instruction tail lives for the same buffer.
1775    #[test]
1776    fn fused_walk_agrees_with_checked_parser() {
1777        let slots = [fresh(7, 1), Slot::Dup(0), fresh(0, 2), fresh(33, 3)];
1778        let ix_data = [5u8, 4, 3, 2, 1];
1779        let mut frame = build_frame(&slots, &ix_data, PID);
1780
1781        let byte_len = frame.words.len() * 8;
1782        // SAFETY: `words` owns `byte_len` initialized bytes.
1783        let bytes: &[u8] =
1784            unsafe { core::slice::from_raw_parts(frame.words.as_ptr() as *const u8, byte_len) };
1785        let checked = parse_instruction_frame_checked(bytes).expect("well-formed");
1786
1787        let mut views = uninit_views::<8>();
1788        // SAFETY: well-formed 8-aligned loader-layout fixture.
1789        let (pid, count, ix) = unsafe { deserialize_accounts::<8>(frame.as_mut_ptr(), &mut views) };
1790
1791        assert_eq!(count, checked.account_count);
1792        assert_eq!(ix, &bytes[checked.instruction_data_range.clone()]);
1793        assert_eq!(
1794            pid.as_array().as_slice(),
1795            &bytes[checked.program_id_offset..checked.program_id_offset + 32]
1796        );
1797    }
1798}
1799
1800// =====================================================================
1801// Kani proof harnesses for the fused entrypoint walk.
1802// =====================================================================
1803//
1804// Three harness families, run by `scripts/kani-native-rawinput.{sh,ps1}`
1805// (CI job `kani-native-rawinput-proofs`):
1806//
1807// (a) **Stride lemma**, `next_record_offset` equals the checked
1808//     `align_offset`-style formula for *every* offset reachable inside
1809//     the SBF input region and every `data_len` up to the loader's
1810//     10 MiB bound, never overflows, always lands 8-aligned, and always
1811//     makes progress. Pure integer proof over the full bounded range.
1812//
1813// (b) **Bounded differential**, for frames with N <= 3 accounts,
1814//     symbolic marker bytes and bounded symbolic `data_len` fields
1815//     (record bodies stay concrete zero to keep CBMC tractable), the
1816//     fused walk's materialized slot pointers, count, instruction-data
1817//     range, and program id equal what the in-file safe oracle
1818//     `parse_instruction_frame_checked` reports. The oracle result is
1819//     *asserted* Ok, never assumed, so a builder/stride bug fails the
1820//     proof instead of vacuously pruning paths. Because the buffers are
1821//     real fixed-size allocations, Kani also model-checks every memory
1822//     access inside the unsafe walk on these paths, against the
1823//     *allocation* bound: these accept-side buffers retain worst-case
1824//     padding slack, so it is the assert-based offset equalities (not
1825//     the allocation edge) that pin the walk's accesses to the oracle's
1826//     frame layout; the byte-exact frame-boundary memory check lives in
1827//     family (c).
1828//
1829// (c) **Trap-before-OOB**, `#[kani::should_panic]` harnesses over
1830//     malformed (self/forward) duplicate markers, with backing buffers
1831//     sized *exactly* to the encoded frame (no worst-case padding), so
1832//     any access even one byte past the legitimate frame is a CBMC
1833//     violation. Precisely, each harness proves two things: the
1834//     `malformed_duplicate_marker` panic is reachable (existential),
1835//     AND no path in the assumed space has a non-panic failure (OOB
1836//     access, invalid write, arithmetic overflow). `should_panic` does
1837//     NOT by itself prove every malformed marker traps. Universal
1838//     rejection is machine-checked only where stated: the assert-based
1839//     `oracle_rejects_exactly_the_malformed_markers` proves the safe
1840//     oracle rejects *every* malformed marker, and the concrete-marker
1841//     slot-zero sub-harnesses are deterministic (single path), making
1842//     their trap verdicts universal for those values. Fused-walk
1843//     universal rejection follows only from the combination of (a),
1844//     (b), and a structural argument; see the family (c) block comment
1845//     for the exact semantics and the residual gap.
1846#[cfg(kani)]
1847mod kani_proofs {
1848    use super::*;
1849
1850    // ── Model constants ─────────────────────────────────────────────
1851
1852    /// Base of the SBF input memory region (`solana-sbpf`'s
1853    /// `ebpf::MM_INPUT_START` = 0x4_0000_0000). This base is 8-aligned,
1854    /// which is the fact `next_record_offset` relies on to fold the
1855    /// absolute-address `align_offset` into relative-offset math.
1856    const MM_INPUT_START: usize = 0x4_0000_0000;
1857
1858    /// Loader bound on serialized account data (10 MiB).
1859    const LOADER_MAX_DATA_LEN: usize = 10_485_760;
1860
1861    /// SBF memory regions are 4 GiB apart, so no byte offset inside the
1862    /// input region can exceed `u32::MAX`.
1863    const MAX_REGION_OFFSET: usize = u32::MAX as usize;
1864
1865    /// Bound on the symbolic per-account `data_len` in the differential
1866    /// harnesses. 8 covers every alignment residue 0..=7 plus one exact
1867    /// stride boundary; family (a) covers the full 10 MiB range.
1868    const MAX_DL: usize = 8;
1869
1870    /// Bound on the symbolic instruction-data length.
1871    const MAX_IX: usize = 8;
1872
1873    /// Worst-case bytes one canonical record consumes when
1874    /// `data_len <= MAX_DL` (a duplicate slot consumes 8 < this).
1875    const RECORD_MAX: usize = next_record_offset(0, MAX_DL);
1876
1877    /// Buffer bytes covering `n` worst-case records plus the count
1878    /// prefix, instruction tail, and program-id trailer.
1879    const fn frame_len(n: usize) -> usize {
1880        8 + n * RECORD_MAX + 8 + MAX_IX + 32
1881    }
1882
1883    /// Recognizable instruction-data filler.
1884    const IX_SENTINEL: [u8; MAX_IX] = [0xA5; MAX_IX];
1885    /// Recognizable program-id trailer.
1886    const PID_SENTINEL: [u8; 32] = [0xC4; 32];
1887    /// One 8-byte word of [`PID_SENTINEL`]. The program id is written and
1888    /// compared a word at a time (see `write_frame` /
1889    /// `check_fused_walk_against_oracle`) so the harness never contains a
1890    /// 32-byte `memcpy`/`memcmp` loop, such a loop would force the whole
1891    /// harness unwind past 32 and blow up the SAT formula. Every real loop
1892    /// then fits in `unwind(10)`, matching the trap/stride harnesses.
1893    const PID_WORD: [u8; 8] = [0xC4; 8];
1894
1895    /// 8-aligned fixed-size backing buffer, mirroring the loader
1896    /// guarantee that the input region starts at the 8-aligned
1897    /// `MM_INPUT_START`.
1898    #[repr(C, align(8))]
1899    struct AlignedBuf<const LEN: usize>([u8; LEN]);
1900
1901    // ── Kani-friendly symbolic values ───────────────────────────────
1902
1903    /// Symbolic marker constrained to the loader's well-formed set for
1904    /// slot `i`: canonical (0xFF) or a strictly-earlier slot index.
1905    fn any_valid_marker(i: usize) -> u8 {
1906        let m: u8 = kani::any();
1907        kani::assume(m == u8::MAX || (m as usize) < i);
1908        m
1909    }
1910
1911    /// Symbolic `data_len` bounded to keep the frame inside `RECORD_MAX`.
1912    fn any_bounded_data_len() -> usize {
1913        let dl: usize = kani::any();
1914        kani::assume(dl <= MAX_DL);
1915        dl
1916    }
1917
1918    /// Symbolic instruction-data length bounded by the sentinel size.
1919    fn any_bounded_ix_len() -> usize {
1920        let n: usize = kani::any();
1921        kani::assume(n <= MAX_IX);
1922        n
1923    }
1924
1925    // ── Kani-friendly frame builder ─────────────────────────────────
1926
1927    /// Serialize a loader input frame into `buf` (which must be zeroed):
1928    /// concrete account count `N`, symbolic marker bytes, bounded
1929    /// symbolic `data_len` fields, concrete-zero record bodies, and
1930    /// sentinel instruction-data / program-id bytes. Returns the
1931    /// exclusive end offset of the encoded frame (one past the program
1932    /// id), which the `trap_frame_layout_is_exact_*` harnesses use to
1933    /// prove the trap-family buffers are sized exactly.
1934    ///
1935    /// Record placement reuses `next_record_offset`, but this is not
1936    /// circular: the accept-side harnesses *assert* (never assume) that
1937    /// the independent bounds-checked oracle accepts the frame and lands
1938    /// on the same offsets, so a stride bug becomes an assertion failure
1939    /// rather than a vacuously-pruned path.
1940    fn write_frame<const N: usize>(
1941        buf: &mut [u8],
1942        markers: &[u8; N],
1943        data_lens: &[usize; N],
1944        ix_len: usize,
1945    ) -> usize {
1946        buf[0..8].copy_from_slice(&(N as u64).to_le_bytes());
1947        let mut pos = 8usize;
1948        let mut i = 0;
1949        while i < N {
1950            buf[pos] = markers[i];
1951            if markers[i] == u8::MAX {
1952                // Canonical record: `data_len` lives at header offset 80.
1953                // Body bytes (data, realloc reserve, padding, rent epoch)
1954                // stay concrete zero to keep CBMC tractable.
1955                buf[pos + 80..pos + 88].copy_from_slice(&(data_lens[i] as u64).to_le_bytes());
1956                pos = next_record_offset(pos, data_lens[i]);
1957            } else {
1958                // Duplicate slot: marker byte + 7 zero padding bytes.
1959                pos += 8;
1960            }
1961            i += 1;
1962        }
1963        buf[pos..pos + 8].copy_from_slice(&(ix_len as u64).to_le_bytes());
1964        pos += 8;
1965        buf[pos..pos + ix_len].copy_from_slice(&IX_SENTINEL[..ix_len]);
1966        pos += ix_len;
1967        // Program id written as 4x 8-byte words (never one 32-byte copy):
1968        // keeps the harness free of any 32-iteration memcpy loop.
1969        let mut w = 0;
1970        while w < 4 {
1971            buf[pos + w * 8..pos + w * 8 + 8].copy_from_slice(&PID_WORD);
1972            w += 1;
1973        }
1974        pos + 32
1975    }
1976
1977    /// Resolve a slot to its canonical record slot by chasing duplicate
1978    /// markers. Terminates because well-formed markers strictly decrease.
1979    fn resolve_canonical<const N: usize>(markers: &[u8; N], mut i: usize) -> usize {
1980        while markers[i] != u8::MAX {
1981            i = markers[i] as usize;
1982        }
1983        i
1984    }
1985
1986    // ── Family (a): stride lemma ────────────────────────────────────
1987
1988    /// For every offset reachable inside the input region and every
1989    /// loader-permitted `data_len`, the folded integer stride equals the
1990    /// checked `align_offset`-style formula (computed on the *absolute*
1991    /// `MM_INPUT_START`-based address), never overflows, stays 8-aligned,
1992    /// and strictly advances. No unwinding concerns: straight-line
1993    /// integer math over the full bounded range.
1994    #[kani::proof]
1995    fn stride_lemma_matches_checked_align_offset_formula() {
1996        let offset: usize = kani::any();
1997        let data_len: usize = kani::any();
1998        kani::assume(offset <= MAX_REGION_OFFSET);
1999        kani::assume(data_len <= LOADER_MAX_DATA_LEN);
2000
2001        // Checked reference: the pre-fusion cursor advance. Every
2002        // `checked_add` doubles as the no-overflow proof.
2003        let unpadded = offset
2004            .checked_add(RuntimeAccount::SIZE)
2005            .and_then(|x| x.checked_add(data_len))
2006            .and_then(|x| x.checked_add(MAX_PERMITTED_DATA_INCREASE))
2007            .expect("pre-alignment cursor must not overflow");
2008        // `align_offset`-style padding on the absolute address, exactly
2009        // what `scan_instruction_frame` computes via `align_offset` and
2010        // `parse_instruction_frame_checked` via `wrapping_neg`.
2011        let absolute = MM_INPUT_START
2012            .checked_add(unpadded)
2013            .expect("absolute address must not overflow");
2014        let pad_absolute = absolute.wrapping_neg() & (BPF_ALIGN_OF_U128 - 1);
2015        // The 8-aligned-base lemma: relative and absolute padding agree.
2016        let pad_relative = unpadded.wrapping_neg() & (BPF_ALIGN_OF_U128 - 1);
2017        assert_eq!(pad_absolute, pad_relative);
2018        let expected = unpadded
2019            .checked_add(pad_absolute)
2020            .and_then(|x| x.checked_add(8))
2021            .expect("aligned cursor must not overflow");
2022
2023        // Kani's built-in overflow checks cover the unchecked `+` chain
2024        // inside `next_record_offset` itself.
2025        let got = next_record_offset(offset, data_len);
2026        assert_eq!(got, expected);
2027        assert_eq!(got & (BPF_ALIGN_OF_U128 - 1), 0);
2028        assert!(got > offset);
2029    }
2030
2031    // ── Family (b): bounded differential vs the safe oracle ────────
2032
2033    /// Accept-side differential body shared by the `deserialize_accounts`
2034    /// harnesses: build a frame with `N` symbolic well-formed slots,
2035    /// require the safe oracle to accept it, run the fused walk with
2036    /// capacity `MAX`, and assert both parsers agree on every observable.
2037    fn check_fused_walk_against_oracle<const N: usize, const MAX: usize, const LEN: usize>() {
2038        let mut markers = [0u8; N];
2039        let mut data_lens = [0usize; N];
2040        let mut i = 0;
2041        while i < N {
2042            markers[i] = any_valid_marker(i);
2043            data_lens[i] = any_bounded_data_len();
2044            i += 1;
2045        }
2046        let ix_len = any_bounded_ix_len();
2047
2048        let mut backing = AlignedBuf::<LEN>([0u8; LEN]);
2049        write_frame::<N>(&mut backing.0, &markers, &data_lens, ix_len);
2050
2051        // Asserted, not assumed: see `write_frame` docs.
2052        let oracle = parse_instruction_frame_checked(&backing.0)
2053            .expect("oracle must accept a well-formed loader frame");
2054        assert_eq!(oracle.account_count, N);
2055        assert_eq!(oracle.instruction_data_range.len(), ix_len);
2056
2057        let base = backing.0.as_ptr() as usize;
2058        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
2059        // state by definition.
2060        let mut views: [MaybeUninit<AccountView<'_>>; MAX] =
2061            unsafe { MaybeUninit::uninit().assume_init() };
2062        // SAFETY: `backing` is an 8-aligned loader-layout buffer built by
2063        // `write_frame` and accepted by the bounds-checked oracle above,
2064        // satisfying the "valid Solana BPF input buffer" contract; Kani
2065        // additionally model-checks every memory access inside the walk.
2066        let (pid, count, ix) =
2067            unsafe { deserialize_accounts::<MAX>(backing.0.as_mut_ptr(), &mut views) };
2068
2069        // Count: the fused walk clamps at MAX (the 254 clamp is
2070        // unreachable for N <= 3).
2071        let expected_count = if N > MAX { MAX } else { N };
2072        assert_eq!(count, expected_count);
2073
2074        // Every materialized slot resolves to exactly the canonical
2075        // record offset the oracle reported.
2076        let mut s = 0;
2077        while s < count {
2078            let canon = resolve_canonical::<N>(&markers, s);
2079            // SAFETY: slots `0..count` were initialized by the fused walk.
2080            let got = unsafe { views[s].assume_init_ref() }.raw_ptr() as usize;
2081            assert_eq!(got - base, oracle.slot_offsets[canon]);
2082            s += 1;
2083        }
2084
2085        // Instruction-data range agrees (start and length), which also
2086        // pins the program-id offset: both parsers read it at the end of
2087        // the instruction data.
2088        assert_eq!(ix.len(), oracle.instruction_data_range.len());
2089        assert_eq!(
2090            ix.as_ptr() as usize - base,
2091            oracle.instruction_data_range.start
2092        );
2093        assert_eq!(oracle.program_id_offset, oracle.instruction_data_range.end);
2094        // Word-wise program-id equality: four u64 comparisons rather than a
2095        // 32-byte slice `==` (which lowers to a `memcmp` loop that would
2096        // force the harness unwind past 32). Reads are scalar; no loop
2097        // exceeds `unwind(10)`.
2098        let pid_bytes = pid.as_array();
2099        let poff = oracle.program_id_offset;
2100        let mut w = 0;
2101        while w < 4 {
2102            let o = w * 8;
2103            let got = u64::from_le_bytes([
2104                pid_bytes[o],
2105                pid_bytes[o + 1],
2106                pid_bytes[o + 2],
2107                pid_bytes[o + 3],
2108                pid_bytes[o + 4],
2109                pid_bytes[o + 5],
2110                pid_bytes[o + 6],
2111                pid_bytes[o + 7],
2112            ]);
2113            let want = u64::from_le_bytes([
2114                backing.0[poff + o],
2115                backing.0[poff + o + 1],
2116                backing.0[poff + o + 2],
2117                backing.0[poff + o + 3],
2118                backing.0[poff + o + 4],
2119                backing.0[poff + o + 5],
2120                backing.0[poff + o + 6],
2121                backing.0[poff + o + 7],
2122            ]);
2123            assert_eq!(got, want);
2124            w += 1;
2125        }
2126    }
2127
2128    #[kani::proof]
2129    // 10 suffices: the program id is written and compared a word at a time
2130    // (see `PID_WORD`), so the harness contains no 32-byte memcpy/memcmp
2131    // loop, every real loop (skip-tail, materialize, 4-word compares) is
2132    // <= 9 iterations. A naive 32-byte slice `==` here previously forced
2133    // the bound past 32 and blew up the SAT formula.
2134    #[kani::unwind(10)]
2135    fn differential_zero_accounts() {
2136        check_fused_walk_against_oracle::<0, 4, { frame_len(0) }>();
2137    }
2138
2139    #[kani::proof]
2140    #[kani::unwind(10)]
2141    fn differential_one_canonical_account() {
2142        check_fused_walk_against_oracle::<1, 4, { frame_len(1) }>();
2143    }
2144
2145    #[kani::proof]
2146    #[kani::unwind(10)]
2147    fn differential_two_accounts_symbolic_markers() {
2148        check_fused_walk_against_oracle::<2, 4, { frame_len(2) }>();
2149    }
2150
2151    #[kani::proof]
2152    #[kani::unwind(10)]
2153    fn differential_three_accounts_symbolic_markers() {
2154        check_fused_walk_against_oracle::<3, 4, { frame_len(3) }>();
2155    }
2156
2157    /// Accounts beyond `MAX` take the skip-only tail: the cursor must
2158    /// still advance record-exactly so the instruction tail is found.
2159    #[kani::proof]
2160    #[kani::unwind(10)]
2161    fn differential_skip_only_tail_beyond_max() {
2162        check_fused_walk_against_oracle::<3, 1, { frame_len(3) }>();
2163    }
2164
2165    /// `deserialize_accounts_fast` shares the stride but never scans the
2166    /// tail; its materialized slots must still match the oracle's.
2167    #[kani::proof]
2168    #[kani::unwind(10)]
2169    fn differential_fast_walk_two_accounts() {
2170        const N: usize = 2;
2171        const LEN: usize = frame_len(N);
2172        let markers = [u8::MAX, any_valid_marker(1)];
2173        let data_lens = [any_bounded_data_len(), any_bounded_data_len()];
2174
2175        let mut backing = AlignedBuf::<LEN>([0u8; LEN]);
2176        write_frame::<N>(&mut backing.0, &markers, &data_lens, 0);
2177
2178        let oracle = parse_instruction_frame_checked(&backing.0)
2179            .expect("oracle must accept a well-formed loader frame");
2180
2181        let base = backing.0.as_ptr() as usize;
2182        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
2183        // state by definition.
2184        let mut views: [MaybeUninit<AccountView<'_>>; 4] =
2185            unsafe { MaybeUninit::uninit().assume_init() };
2186        static EMPTY_IX: [u8; 0] = [];
2187        let program_id = Address::new_from_array(PID_SENTINEL);
2188        // SAFETY: same oracle-validated 8-aligned loader-layout buffer
2189        // contract as `check_fused_walk_against_oracle`; instruction data
2190        // and program id are supplied out of band per the fast-path
2191        // contract and are opaque pass-throughs to this walk.
2192        let (pid, count, ix) = unsafe {
2193            deserialize_accounts_fast::<4>(
2194                backing.0.as_mut_ptr(),
2195                &mut views,
2196                &EMPTY_IX,
2197                &program_id,
2198            )
2199        };
2200        assert_eq!(count, N);
2201        assert_eq!(ix.len(), 0);
2202        assert_eq!(pid.as_array(), &PID_SENTINEL);
2203
2204        let mut s = 0;
2205        while s < count {
2206            let canon = resolve_canonical::<N>(&markers, s);
2207            // SAFETY: slots `0..count` were initialized by the fast walk.
2208            let got = unsafe { views[s].assume_init_ref() }.raw_ptr() as usize;
2209            assert_eq!(got - base, oracle.slot_offsets[canon]);
2210            s += 1;
2211        }
2212    }
2213
2214    /// `scan_instruction_frame` (the lazy-path scanner, which still uses
2215    /// pointer `align_offset` internally) must locate the same account
2216    /// span and instruction tail as the oracle.
2217    #[kani::proof]
2218    #[kani::unwind(10)]
2219    fn differential_scan_frame_two_accounts() {
2220        const N: usize = 2;
2221        const LEN: usize = frame_len(N);
2222        let markers = [u8::MAX, any_valid_marker(1)];
2223        let data_lens = [any_bounded_data_len(), any_bounded_data_len()];
2224        let ix_len = any_bounded_ix_len();
2225
2226        let mut backing = AlignedBuf::<LEN>([0u8; LEN]);
2227        write_frame::<N>(&mut backing.0, &markers, &data_lens, ix_len);
2228
2229        let oracle = parse_instruction_frame_checked(&backing.0)
2230            .expect("oracle must accept a well-formed loader frame");
2231
2232        let base = backing.0.as_ptr() as usize;
2233        // SAFETY: same oracle-validated 8-aligned loader-layout buffer
2234        // contract as `check_fused_walk_against_oracle`.
2235        let frame = unsafe { scan_instruction_frame(backing.0.as_mut_ptr()) };
2236
2237        assert_eq!(frame.account_count, N);
2238        assert_eq!(frame.accounts_start as usize - base, 8);
2239        assert_eq!(
2240            frame.instruction_data.len(),
2241            oracle.instruction_data_range.len()
2242        );
2243        assert_eq!(
2244            frame.instruction_data.as_ptr() as usize - base,
2245            oracle.instruction_data_range.start
2246        );
2247        assert_eq!(
2248            frame.program_id.as_array().as_slice(),
2249            &backing.0[oracle.program_id_offset..oracle.program_id_offset + 32]
2250        );
2251    }
2252
2253    // ── Family (c): trap-before-OOB on malformed markers ───────────
2254    //
2255    // Proof semantics, stated precisely. `#[kani::should_panic]` is
2256    // EXISTENTIAL on the panic side: a harness verifies iff
2257    //   (1) at least one path in the assumed input space panics, and
2258    //   (2) NO path exhibits a non-panic property failure, an
2259    //       out-of-bounds read/write, an invalid `accounts[]` write, or
2260    //       an arithmetic overflow is a verification FAILURE, because
2261    //       those are not panics.
2262    // Clause (2) holds on EVERY path; clause (1) alone does NOT prove
2263    // that every malformed marker traps, a hypothetical path that
2264    // silently *returned* for some malformed marker would still verify.
2265    // Universal statements are machine-checked only where noted:
2266    //   * `oracle_rejects_exactly_the_malformed_markers` is assert-based
2267    //     (no `should_panic`), so it proves the safe oracle rejects
2268    //     EVERY malformed marker in the symbolic space;
2269    //   * the `trap_slot_zero_marker_*` sub-harnesses each fix one
2270    //     CONCRETE marker, making execution deterministic (one path),
2271    //     so their `should_panic` verdicts are universal for those
2272    //     specific marker values;
2273    //   * "the walk traps on every malformed marker it would turn into a
2274    //     view, on every path" is NOT established by any single harness
2275    //     here. It follows in combination: family (b) pins the accept
2276    //     side to the oracle, the oracle harness pins the reject set, the
2277    //     stride lemma (a) pins the cursor, and structurally the
2278    //     materializing walk's only non-trapping branch for a non-0xFF
2279    //     marker is `duplicate_of < slot`, which the harness assumptions
2280    //     exclude. That final step is a source-level argument, not a
2281    //     CBMC check. Records past the bound are never viewed; the
2282    //     skip-only tail crosses them by size alone, and the
2283    //     `skip_only_tail_crosses_*` harnesses prove it stays in bounds.
2284    //
2285    // Exact allocation, the mechanism every trap harness below uses
2286    // (this is what makes clause (2) sharp): each backing buffer is
2287    // sized TO THE BYTE of the encoded malformed frame, with no
2288    // worst-case padding, so a read or write even one byte past the
2289    // legitimate frame is a CBMC violation instead of slack absorbed by
2290    // an oversized allocation. The two-slot frame's length depends on
2291    // the symbolic `data_len` only through u128 alignment: `dl == 0`
2292    // needs one 8-byte padding step fewer than `dl` in `1..=MAX_DL`,
2293    // which all encode to the same length (compile-time-checked below).
2294    // Each two-slot trap harness is therefore split into exactly two
2295    // size classes, `_dl0` (concrete `dl = 0`) and `_dl_nonzero`
2296    // (symbolic `dl` in `1..=MAX_DL`), each with an exactly-sized
2297    // buffer; together they cover the same `0..=MAX_DL` space the
2298    // padded originals did. The slot-zero frame has no `data_len` at
2299    // all, so a single exact size covers it.
2300    //
2301    // The `trap_frame_layout_is_exact_*` companions prove, assert-based
2302    // over the SAME symbolic space, that the builder fills each buffer
2303    // exactly (`end == LEN`) and never panics while doing so; so a
2304    // `should_panic` trap harness cannot pass vacuously via a builder
2305    // panic or leave hidden slack.
2306    //
2307    // The trap harnesses themselves are deliberately assertion-free: an
2308    // `assert!` before the call would itself panic on failure and be
2309    // masked by `should_panic`.
2310
2311    /// Exact encoded length of the canonical-then-malformed two-slot
2312    /// trap frame: 8-byte count prefix, canonical record 0 starting at
2313    /// offset 8 with `data_len = dl`, 8-byte malformed duplicate slot,
2314    /// 8-byte instruction-data length (zero, no data bytes), 32-byte
2315    /// program id.
2316    const fn trap_frame_len(dl: usize) -> usize {
2317        next_record_offset(8, dl) + 8 + 8 + 32
2318    }
2319
2320    /// Two-slot trap frame length for the `dl = 0` size class.
2321    const TRAP_LEN_DL0: usize = trap_frame_len(0);
2322    /// Two-slot trap frame length shared by every `dl` in `1..=MAX_DL`
2323    /// (u128 alignment folds them all to one size).
2324    const TRAP_LEN_DL_NONZERO: usize = trap_frame_len(1);
2325    /// Exact encoded length of the one-slot slot-zero trap frame:
2326    /// count prefix + 8-byte duplicate slot + ix-len prefix + program id.
2327    const TRAP_LEN_SLOT_ZERO: usize = 8 + 8 + 8 + 32;
2328
2329    // Compile-time proof that the two size classes are exhaustive over
2330    // `0..=MAX_DL`: every nonzero `dl` encodes to `TRAP_LEN_DL_NONZERO`
2331    // and `dl = 0` is strictly its own (smaller) class.
2332    const _: () = {
2333        let mut dl = 1;
2334        while dl <= MAX_DL {
2335            assert!(trap_frame_len(dl) == TRAP_LEN_DL_NONZERO);
2336            dl += 1;
2337        }
2338        assert!(TRAP_LEN_DL0 < TRAP_LEN_DL_NONZERO);
2339    };
2340
2341    /// Build the canonical-then-malformed two-slot trap frame: slot 0 is
2342    /// canonical with symbolic `data_len` drawn from `dl_min..=dl_max`
2343    /// (one exact-size class), slot 1 carries a symbolic malformed
2344    /// marker (`!= 0xFF`, `>= 1`, i.e. self or forward reference at
2345    /// slot 1). Returns the buffer and the builder's exclusive end
2346    /// offset; the `trap_frame_layout_is_exact_*` harnesses assert
2347    /// `end == LEN` over this same symbolic space.
2348    fn build_two_slot_trap_frame<const LEN: usize>(
2349        dl_min: usize,
2350        dl_max: usize,
2351    ) -> (AlignedBuf<LEN>, usize) {
2352        let bad: u8 = kani::any();
2353        kani::assume(bad != u8::MAX && bad as usize >= 1);
2354        let dl: usize = kani::any();
2355        kani::assume(dl >= dl_min && dl <= dl_max);
2356
2357        let mut backing = AlignedBuf::<LEN>([0u8; LEN]);
2358        let end = write_frame::<2>(&mut backing.0, &[u8::MAX, bad], &[dl, 0], 0);
2359        (backing, end)
2360    }
2361
2362    /// Build the one-slot slot-zero trap frame whose sole slot carries
2363    /// `marker` (symbolic or concrete; the caller guarantees it is not
2364    /// 0xFF, so the slot encodes as an 8-byte duplicate slot).
2365    fn build_slot_zero_trap_frame(marker: u8) -> (AlignedBuf<TRAP_LEN_SLOT_ZERO>, usize) {
2366        let mut backing = AlignedBuf::<TRAP_LEN_SLOT_ZERO>([0u8; TRAP_LEN_SLOT_ZERO]);
2367        let end = write_frame::<1>(&mut backing.0, &[marker], &[0], 0);
2368        (backing, end)
2369    }
2370
2371    // Assert-based (NOT should_panic) exactness companions: over the
2372    // same symbolic space as the trap harnesses, the builder terminates
2373    // without panicking and fills the buffer to exactly `LEN` bytes.
2374    // These close the two vacuity holes of the trap family: a builder
2375    // panic masked by `should_panic`, and hidden slack past the frame.
2376
2377    #[kani::proof]
2378    #[kani::unwind(10)]
2379    fn trap_frame_layout_is_exact_dl0() {
2380        let (_backing, end) = build_two_slot_trap_frame::<TRAP_LEN_DL0>(0, 0);
2381        assert_eq!(end, TRAP_LEN_DL0);
2382    }
2383
2384    #[kani::proof]
2385    #[kani::unwind(10)]
2386    fn trap_frame_layout_is_exact_dl_nonzero() {
2387        let (_backing, end) = build_two_slot_trap_frame::<TRAP_LEN_DL_NONZERO>(1, MAX_DL);
2388        assert_eq!(end, TRAP_LEN_DL_NONZERO);
2389    }
2390
2391    #[kani::proof]
2392    #[kani::unwind(10)]
2393    fn trap_frame_layout_is_exact_slot_zero() {
2394        let marker: u8 = kani::any();
2395        kani::assume(marker != u8::MAX);
2396        let (_backing, end) = build_slot_zero_trap_frame(marker);
2397        assert_eq!(end, TRAP_LEN_SLOT_ZERO);
2398    }
2399
2400    /// Shared trap body: run `deserialize_accounts::<MAX>` on one
2401    /// exact-size malformed two-slot frame class. `MAX >= 2` puts the
2402    /// malformed slot 1 in the materialize range, where it must trap.
2403    fn trap_deserialize_two_slot<const MAX: usize, const LEN: usize>(dl_min: usize, dl_max: usize) {
2404        let (mut backing, _end) = build_two_slot_trap_frame::<LEN>(dl_min, dl_max);
2405        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
2406        // state by definition.
2407        let mut views: [MaybeUninit<AccountView<'_>>; MAX] =
2408            unsafe { MaybeUninit::uninit().assume_init() };
2409        // SAFETY: 8-aligned loader-layout buffer sized exactly to the
2410        // encoded frame (`trap_frame_layout_is_exact_*`); the malformed
2411        // marker is the condition under test and must trap before any
2412        // access past the frame end, Kani checks every access on every
2413        // path of this harness against that exact allocation boundary.
2414        let _ = unsafe { deserialize_accounts::<MAX>(backing.0.as_mut_ptr(), &mut views) };
2415    }
2416
2417    #[kani::proof]
2418    #[kani::unwind(10)]
2419    #[kani::should_panic]
2420    fn trap_fires_on_malformed_marker_in_materialize_range_dl0() {
2421        trap_deserialize_two_slot::<4, TRAP_LEN_DL0>(0, 0);
2422    }
2423
2424    #[kani::proof]
2425    #[kani::unwind(10)]
2426    #[kani::should_panic]
2427    fn trap_fires_on_malformed_marker_in_materialize_range_dl_nonzero() {
2428        trap_deserialize_two_slot::<4, TRAP_LEN_DL_NONZERO>(1, MAX_DL);
2429    }
2430
2431    // MAX = 1 pushes the malformed slot 1 into the skip-only tail. No view
2432    // is made there, so the walk crosses the 8-byte record whatever its
2433    // marker names. Assert-based over the whole symbolic marker space and
2434    // on the exact-size buffer, so a read one byte past the frame fails the
2435    // proof.
2436
2437    /// Shared body: the skip-only tail crosses a malformed slot 1 and lands
2438    /// on the instruction tail.
2439    fn skip_tail_crosses_two_slot<const LEN: usize>(dl_min: usize, dl_max: usize) {
2440        let (mut backing, _end) = build_two_slot_trap_frame::<LEN>(dl_min, dl_max);
2441        let base = backing.0.as_ptr() as usize;
2442        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
2443        // state by definition.
2444        let mut views: [MaybeUninit<AccountView<'_>>; 1] =
2445            unsafe { MaybeUninit::uninit().assume_init() };
2446        // SAFETY: 8-aligned loader-layout buffer sized exactly to the
2447        // encoded frame (`trap_frame_layout_is_exact_*`).
2448        let (_, count, ix) =
2449            unsafe { deserialize_accounts::<1>(backing.0.as_mut_ptr(), &mut views) };
2450        assert_eq!(count, 1);
2451        assert_eq!(ix.len(), 0);
2452        assert_eq!(ix.as_ptr() as usize - base, LEN - 32);
2453    }
2454
2455    #[kani::proof]
2456    #[kani::unwind(10)]
2457    fn skip_only_tail_crosses_malformed_marker_dl0() {
2458        skip_tail_crosses_two_slot::<TRAP_LEN_DL0>(0, 0);
2459    }
2460
2461    #[kani::proof]
2462    #[kani::unwind(10)]
2463    fn skip_only_tail_crosses_malformed_marker_dl_nonzero() {
2464        skip_tail_crosses_two_slot::<TRAP_LEN_DL_NONZERO>(1, MAX_DL);
2465    }
2466
2467    /// Shared trap body for `deserialize_accounts_fast` on one
2468    /// exact-size malformed two-slot frame class.
2469    fn trap_fast_walk_two_slot<const LEN: usize>(dl_min: usize, dl_max: usize) {
2470        let (mut backing, _end) = build_two_slot_trap_frame::<LEN>(dl_min, dl_max);
2471        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
2472        // state by definition.
2473        let mut views: [MaybeUninit<AccountView<'_>>; 4] =
2474            unsafe { MaybeUninit::uninit().assume_init() };
2475        static EMPTY_IX: [u8; 0] = [];
2476        let program_id = Address::new_from_array(PID_SENTINEL);
2477        // SAFETY: 8-aligned loader-layout buffer sized exactly to the
2478        // encoded frame, with out-of-band tail per the fast-path
2479        // contract; the malformed marker is the condition under test and
2480        // must trap before any access past the frame end, Kani checks
2481        // every access on every path against that exact allocation
2482        // boundary.
2483        let _ = unsafe {
2484            deserialize_accounts_fast::<4>(
2485                backing.0.as_mut_ptr(),
2486                &mut views,
2487                &EMPTY_IX,
2488                &program_id,
2489            )
2490        };
2491    }
2492
2493    #[kani::proof]
2494    #[kani::unwind(10)]
2495    #[kani::should_panic]
2496    fn trap_fires_in_fast_walk_dl0() {
2497        trap_fast_walk_two_slot::<TRAP_LEN_DL0>(0, 0);
2498    }
2499
2500    #[kani::proof]
2501    #[kani::unwind(10)]
2502    #[kani::should_panic]
2503    fn trap_fires_in_fast_walk_dl_nonzero() {
2504        trap_fast_walk_two_slot::<TRAP_LEN_DL_NONZERO>(1, MAX_DL);
2505    }
2506
2507    /// Shared trap body for `scan_instruction_frame` on one exact-size
2508    /// malformed two-slot frame class.
2509    fn trap_scan_frame_two_slot<const LEN: usize>(dl_min: usize, dl_max: usize) {
2510        let (mut backing, _end) = build_two_slot_trap_frame::<LEN>(dl_min, dl_max);
2511        // SAFETY: 8-aligned loader-layout buffer sized exactly to the
2512        // encoded frame; the malformed marker is the condition under test
2513        // and must trap before any access past the frame end, Kani
2514        // checks every access on every path against that exact
2515        // allocation boundary.
2516        let _ = unsafe { scan_instruction_frame(backing.0.as_mut_ptr()) };
2517    }
2518
2519    #[kani::proof]
2520    #[kani::unwind(10)]
2521    #[kani::should_panic]
2522    fn trap_fires_in_scan_frame_dl0() {
2523        trap_scan_frame_two_slot::<TRAP_LEN_DL0>(0, 0);
2524    }
2525
2526    #[kani::proof]
2527    #[kani::unwind(10)]
2528    #[kani::should_panic]
2529    fn trap_fires_in_scan_frame_dl_nonzero() {
2530        trap_scan_frame_two_slot::<TRAP_LEN_DL_NONZERO>(1, MAX_DL);
2531    }
2532
2533    /// Shared body for the slot-zero trap harnesses: slot 0 has no
2534    /// earlier slot, so every non-canonical marker value is malformed
2535    /// there. Exact-size buffer, no `data_len` dimension at all.
2536    fn trap_slot_zero(marker: u8) {
2537        let (mut backing, _end) = build_slot_zero_trap_frame(marker);
2538        // SAFETY: an array of `MaybeUninit` is valid in the uninitialized
2539        // state by definition.
2540        let mut views: [MaybeUninit<AccountView<'_>>; 4] =
2541            unsafe { MaybeUninit::uninit().assume_init() };
2542        // SAFETY: 8-aligned loader-layout buffer sized exactly to the
2543        // encoded frame (`trap_frame_layout_is_exact_slot_zero`); the
2544        // malformed marker is the condition under test and must trap
2545        // before any access past the frame end, Kani checks every
2546        // access on every path against that exact allocation boundary.
2547        let _ = unsafe { deserialize_accounts::<4>(backing.0.as_mut_ptr(), &mut views) };
2548    }
2549
2550    /// Existential over the full symbolic malformed-marker space at
2551    /// slot 0 (see the family (c) comment for exactly what that means).
2552    #[kani::proof]
2553    #[kani::unwind(10)]
2554    #[kani::should_panic]
2555    fn trap_fires_on_any_duplicate_marker_at_slot_zero() {
2556        let bad: u8 = kani::any();
2557        kani::assume(bad != u8::MAX);
2558        trap_slot_zero(bad);
2559    }
2560
2561    // Per-concrete-value slot-zero sub-harnesses: with every input byte
2562    // concrete, execution is deterministic, a single path; so each
2563    // `should_panic` verdict below is UNIVERSAL for that marker value
2564    // (the walk provably traps on it), not merely existential.
2565
2566    #[kani::proof]
2567    #[kani::unwind(10)]
2568    #[kani::should_panic]
2569    fn trap_slot_zero_marker_0x00_self_reference() {
2570        trap_slot_zero(0x00);
2571    }
2572
2573    #[kani::proof]
2574    #[kani::unwind(10)]
2575    #[kani::should_panic]
2576    fn trap_slot_zero_marker_0x01_forward_reference() {
2577        trap_slot_zero(0x01);
2578    }
2579
2580    #[kani::proof]
2581    #[kani::unwind(10)]
2582    #[kani::should_panic]
2583    fn trap_slot_zero_marker_0xfe_max_forward_reference() {
2584        trap_slot_zero(0xFE);
2585    }
2586
2587    /// Oracle side of the rejection story, and the only harness in this
2588    /// module that machine-checks a UNIVERSAL rejection property: it is
2589    /// assert-based (no `should_panic`), so over *fully* symbolic
2590    /// markers for a two-slot frame it proves the safe parser accepts
2591    /// iff both markers are well-formed, and every rejection is
2592    /// precisely `MalformedDuplicateMarker`, on every path. Combined
2593    /// with family (b) (well-formed => both parsers accept, outputs
2594    /// equal) and the family (c) trap harnesses (existential trap
2595    /// reachability + no memory-safety failure on any assumed path,
2596    /// against exact-size buffers), this supports; but note, per the
2597    /// family (c) comment, does not single-handedly machine-check,
2598    /// "both reject exactly the same inputs" for the marker dimension.
2599    #[kani::proof]
2600    #[kani::unwind(10)]
2601    fn oracle_rejects_exactly_the_malformed_markers() {
2602        const LEN: usize = frame_len(2);
2603        let m0: u8 = kani::any();
2604        let m1: u8 = kani::any();
2605        let data_lens = [any_bounded_data_len(), any_bounded_data_len()];
2606        let ix_len = any_bounded_ix_len();
2607
2608        let mut backing = AlignedBuf::<LEN>([0u8; LEN]);
2609        write_frame::<2>(&mut backing.0, &[m0, m1], &data_lens, ix_len);
2610
2611        let result = parse_instruction_frame_checked(&backing.0);
2612        let well_formed = m0 == u8::MAX && (m1 == u8::MAX || m1 == 0);
2613        assert_eq!(result.is_ok(), well_formed);
2614        if let Err(err) = result {
2615            assert!(matches!(err, FrameError::MalformedDuplicateMarker { .. }));
2616        }
2617    }
2618}
2619
2620#[cfg(test)]
2621mod loader_count_tests {
2622    /// The count-exact entrypoint compares this word with the arm's bound
2623    /// before walking; it is the loader's first eight bytes, little-endian.
2624    #[test]
2625    fn reads_the_leading_count_word() {
2626        let mut input = [0u8; 16];
2627        input[0..8].copy_from_slice(&5u64.to_le_bytes());
2628        // SAFETY: `input` holds the count word and eight more readable bytes.
2629        assert_eq!(unsafe { super::loader_account_count(input.as_ptr()) }, 5);
2630        input[0..8].copy_from_slice(&0u64.to_le_bytes());
2631        // SAFETY: as above.
2632        assert_eq!(unsafe { super::loader_account_count(input.as_ptr()) }, 0);
2633    }
2634}