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