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