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