hopper_native/entrypoint.rs
1//! Program entrypoint ownership for Hopper Native.
2//!
3//! This file is the only raw program-entry boundary owner in Hopper Native.
4//! Loader input parsing lives in [`crate::raw_input`], while the public macros
5//! below own the raw `entrypoint(input: *mut u8)` boundary and delegate into
6//! Hopper callbacks.
7
8use core::mem::MaybeUninit;
9
10use crate::account_view::AccountView;
11use crate::address::Address;
12use crate::error::ProgramError;
13
14/// Convert a handler's `ProgramError` into the Solana runtime's u64 return code.
15///
16/// Outlined `#[cold] #[inline(never)]` so an entrypoint's success tail lowers to
17/// a bare `return SUCCESS` and the `ProgramError -> u64` mapping (the 25-arm
18/// `From<ProgramError> for u64` match) is never inlined into the hot frame,
19/// where it would add code size and stack traffic that every successful
20/// invocation pays for. This mirrors Pinocchio's cold error outline.
21///
22/// The conversion is exactly `Into::<u64>::into(e)`, byte-for-byte identical to
23/// the previous inline `error.into()`, so the runtime error codes are unchanged.
24#[cold]
25#[inline(never)]
26pub fn err_to_u64(e: ProgramError) -> u64 {
27 e.into()
28}
29
30/// Process the BPF entrypoint input.
31///
32/// This is the function called by the canonical Hopper Native entrypoint macro's
33/// generated entrypoint.
34///
35/// # Safety
36///
37/// `input` must be the raw pointer provided by the Solana runtime.
38#[inline(always)]
39pub unsafe fn process_entrypoint<const MAX: usize>(
40 input: *mut u8,
41 process_instruction: for<'info> fn(
42 &'info Address,
43 &'info [AccountView<'info>],
44 &'info [u8],
45 ) -> crate::ProgramResult,
46) -> u64 {
47 const UNINIT: MaybeUninit<AccountView<'static>> = MaybeUninit::uninit();
48 let mut accounts = [UNINIT; 254]; // MAX_TX_ACCOUNTS
49
50 let (program_id, count, instruction_data) =
51 // SAFETY: `input` is the loader's input buffer (this function's
52 // contract) and `accounts` has room for the 254 accounts the parser
53 // may write.
54 unsafe { crate::raw_input::deserialize_accounts::<254>(input, &mut accounts) };
55
56 // Respect MAX: only pass up to MAX accounts to the callback.
57 let effective_count = count.min(MAX);
58 // SAFETY: The parser initialized the first `count` slots and
59 // `effective_count <= count`; `MaybeUninit<AccountView>` has the layout
60 // of `AccountView`.
61 let account_slice = unsafe {
62 core::slice::from_raw_parts(accounts.as_ptr() as *const AccountView<'_>, effective_count)
63 };
64
65 match process_instruction(program_id, account_slice, instruction_data) {
66 Ok(()) => crate::SUCCESS,
67 Err(error) => err_to_u64(error),
68 }
69}
70
71/// Declare the canonical Hopper Native program entrypoint.
72///
73/// Generates the `extern "C" fn entrypoint` that the Solana runtime calls.
74/// `program_entrypoint!` remains available as a backward-compatible alias.
75///
76/// # Usage
77///
78/// ```ignore
79/// use hopper_native::hopper_program_entrypoint;
80///
81/// hopper_program_entrypoint!(process_instruction);
82///
83/// pub fn process_instruction(
84/// program_id: &Address,
85/// accounts: &[AccountView],
86/// instruction_data: &[u8],
87/// ) -> ProgramResult {
88/// Ok(())
89/// }
90/// ```
91#[macro_export]
92macro_rules! hopper_program_entrypoint {
93 ( $process_instruction:expr ) => {
94 $crate::hopper_program_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
95 };
96 ( $process_instruction:expr, $maximum:expr ) => {
97 /// # Safety
98 ///
99 /// Called by the Solana runtime; `input` is a valid BPF input buffer.
100 #[no_mangle]
101 pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
102 const UNINIT: core::mem::MaybeUninit<$crate::AccountView<'static>> =
103 core::mem::MaybeUninit::<$crate::AccountView<'static>>::uninit();
104 let mut accounts = [UNINIT; $maximum];
105
106 // SAFETY: `input` is the loader's input buffer (the entrypoint's
107 // contract) and `accounts` has `$maximum` slots, the bound the
108 // parser is given.
109 let (program_id, count, instruction_data) = unsafe {
110 $crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts)
111 };
112
113 match $process_instruction(
114 program_id,
115 // SAFETY: The parser initialized the first `count` slots;
116 // `MaybeUninit<AccountView>` has the layout of `AccountView`.
117 unsafe {
118 core::slice::from_raw_parts(
119 accounts.as_ptr() as *const $crate::AccountView<'_>,
120 count,
121 )
122 },
123 instruction_data,
124 ) {
125 Ok(()) => $crate::SUCCESS,
126 Err(error) => $crate::entrypoint::err_to_u64(error),
127 }
128 }
129 };
130}
131
132/// Backward-compatible alias for `hopper_program_entrypoint!`.
133#[macro_export]
134macro_rules! program_entrypoint {
135 ( $process_instruction:expr ) => {
136 $crate::hopper_program_entrypoint!($process_instruction);
137 };
138 ( $process_instruction:expr, $maximum:expr ) => {
139 $crate::hopper_program_entrypoint!($process_instruction, $maximum);
140 };
141}
142
143/// Declare a fast two-argument Hopper Native program entrypoint.
144///
145/// Uses the SVM's second entrypoint register (`r2`), which carries a
146/// direct pointer to instruction data under [SIMD-0321], letting the
147/// entrypoint skip locating the instruction tail. Measured honestly
148/// (2026-07-21, post the 2026-07-07 fused single-pass walk): the fused
149/// scanning entrypoint already hops records by their `data_len` headers
150/// without touching account data, so on programs whose accounts fit the
151/// declared maximum the r2 path is CU-neutral (+/- 2 CU in controlled
152/// A/Bs) and costs ~368 bytes for carrying both paths. The historical
153/// "~30-40 CU" figure described the pre-fusion two-pass scanner. The r2
154/// path earns its keep as the base of the SIMD-0449 O(1) account-pointer
155/// table, and for instructions whose transaction carries many more
156/// accounts than the program materializes.
157///
158/// # Feature gating (`simd-0321`)
159///
160/// SIMD-0321 is **activated on all three public clusters** (feature gate
161/// `5xXZc66h4UdB6Yq7FzdBxBiRAFMMScMLwHxk2QZDaNZL`; mainnet-beta at slot
162/// 410,400,000, 2026-04-01). Current agave sets `r2` unconditionally, so
163/// builds may enable the feature for any cluster target; on a runtime
164/// that ever leaves `r2` zero, the null-check below still falls back to
165/// the scanning parse.
166///
167/// - **Default (feature off):** this macro expands to the standard
168/// scanning entrypoint ([`hopper_program_entrypoint!`]). Identical
169/// semantics, sound on every cluster today, and source-compatible:
170/// when the gate activates, rebuild with the feature to claim the
171/// CU savings.
172/// - **`simd-0321` enabled:** the macro expands to the two-argument
173/// entrypoint. As defense in depth it null-checks `r2` and falls
174/// back to the scanning parse when the register is zero (current
175/// SBPF VMs zero-initialize unused argument registers), so a binary
176/// built with the feature degrades to the slow path instead of
177/// reading garbage if it lands on a cluster without the activation.
178///
179/// `hopper doctor` / `hopper deploy` can check the feature-gate account
180/// on the target cluster before a `simd-0321` build ships.
181///
182/// [SIMD-0321]: https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0321-vm-r2-instruction-data-pointer.md
183///
184/// # Usage
185///
186/// ```ignore
187/// use hopper_native::hopper_fast_entrypoint;
188///
189/// hopper_fast_entrypoint!(process_instruction, 3);
190///
191/// pub fn process_instruction(
192/// program_id: &Address,
193/// accounts: &[AccountView],
194/// instruction_data: &[u8],
195/// ) -> ProgramResult {
196/// Ok(())
197/// }
198/// ```
199#[cfg(feature = "simd-0321")]
200#[macro_export]
201macro_rules! hopper_fast_entrypoint {
202 ( $process_instruction:expr ) => {
203 $crate::hopper_fast_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
204 };
205 ( $process_instruction:expr, $maximum:expr ) => {
206 /// # Safety
207 ///
208 /// Called by the Solana runtime; `input` is a valid BPF input buffer.
209 /// When SIMD-0321 is active, `ix_data` points to the instruction data
210 /// with its u64 length stored at offset -8; when it is not active the
211 /// register is zero and the scanning fallback below is taken.
212 #[no_mangle]
213 pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data: *const u8) -> u64 {
214 const UNINIT: core::mem::MaybeUninit<$crate::AccountView<'static>> =
215 core::mem::MaybeUninit::<$crate::AccountView<'static>>::uninit();
216 let mut accounts = [UNINIT; $maximum];
217
218 let (program_id, count, instruction_data) = if ix_data.is_null() {
219 // SIMD-0321 not active on this cluster: r2 is zero. Fall back
220 // to the full scanning parse so the program stays correct.
221 // SAFETY: `input` is the loader-provided input buffer; the
222 // scanning parser owns all bounds/duplicate-marker checks.
223 unsafe { $crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts) }
224 } else {
225 // Instruction data length is the u64 immediately before the
226 // data pointer (per SIMD-0321's serialization contract).
227 // SAFETY: SIMD-0321 ix_data points at instruction-data bytes
228 // with u64 length prefix at `ix_data - 8`.
229 let ix_len =
230 unsafe { core::ptr::read_unaligned(ix_data.sub(8) as *const u64) as usize };
231 let instruction_data: &'static [u8] =
232 unsafe { core::slice::from_raw_parts(ix_data, ix_len) };
233
234 // SAFETY: program id trails the instruction data per the
235 // loader serialization layout; `Address` is a transparent
236 // `[u8; 32]`, so a reference into the buffer is valid at any
237 // offset and lives as long as the invocation.
238 let program_id: &'static $crate::Address =
239 unsafe { &*(ix_data.add(ix_len) as *const $crate::Address) };
240
241 if $crate::raw_input::SIMD_0449_TABLE_ENABLED {
242 // SIMD-0449 build: consume the runtime's appended
243 // pre-deduplicated account-pointer table, O(1)
244 // resolution plus one pointer copy per account. The
245 // gate is a `const`, so the untaken branch folds
246 // away entirely.
247 // SAFETY: the `simd-0449` feature asserts the SIMD
248 // is active on the target cluster (table present);
249 // `instruction_data`/`program_id` were derived from
250 // the SIMD-0321 r2 register above.
251 unsafe {
252 $crate::raw_input::deserialize_accounts_0449_into::<$maximum>(
253 input,
254 &mut accounts,
255 instruction_data,
256 program_id,
257 )
258 }
259 } else {
260 // SAFETY: `input` is the loader input buffer; account-slot
261 // framing is validated by `deserialize_accounts_fast`.
262 unsafe {
263 $crate::raw_input::deserialize_accounts_fast::<$maximum>(
264 input,
265 &mut accounts,
266 instruction_data,
267 program_id,
268 )
269 }
270 }
271 };
272
273 match $process_instruction(
274 program_id,
275 // SAFETY: the first `count` slots were initialized by the
276 // parser above; `AccountView` is repr(C) over the slot data.
277 unsafe {
278 core::slice::from_raw_parts(
279 accounts.as_ptr() as *const $crate::AccountView<'_>,
280 count,
281 )
282 },
283 instruction_data,
284 ) {
285 Ok(()) => $crate::SUCCESS,
286 Err(error) => $crate::entrypoint::err_to_u64(error),
287 }
288 }
289 };
290}
291
292/// Without the `simd-0321` feature the "fast" entrypoint is an alias for
293/// the standard scanning entrypoint. The SIMD-0321 gate is live on every
294/// public cluster (mainnet-beta 2026-04-01); the r2 form is sound to build
295/// and stays opt-in only because it measured CU-neutral against the fused
296/// scanning walk for ~368 bytes of extra `.text`. The two-argument r2 form
297/// also null-checks the register and falls back to scanning, so it is safe
298/// even where the gate is somehow inactive.
299#[cfg(not(feature = "simd-0321"))]
300#[macro_export]
301macro_rules! hopper_fast_entrypoint {
302 ( $process_instruction:expr ) => {
303 $crate::hopper_program_entrypoint!($process_instruction);
304 };
305 ( $process_instruction:expr, $maximum:expr ) => {
306 $crate::hopper_program_entrypoint!($process_instruction, $maximum);
307 };
308}
309
310/// Backward-compatible alias for `hopper_fast_entrypoint!`.
311#[macro_export]
312macro_rules! fast_entrypoint {
313 ( $process_instruction:expr ) => {
314 $crate::hopper_fast_entrypoint!($process_instruction);
315 };
316 ( $process_instruction:expr, $maximum:expr ) => {
317 $crate::hopper_fast_entrypoint!($process_instruction, $maximum);
318 };
319}
320
321/// Declare the canonical lazy program entrypoint that defers account parsing.
322#[macro_export]
323macro_rules! hopper_lazy_entrypoint {
324 ( $process:expr ) => {
325 /// # Safety
326 ///
327 /// Called by the Solana runtime; `input` is a valid BPF input buffer.
328 #[no_mangle]
329 pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
330 // SAFETY: `input` is the loader's input buffer (the entrypoint's
331 // contract), which is what `lazy_deserialize` requires.
332 let mut ctx = unsafe { $crate::lazy::lazy_deserialize(input) };
333 match $process(&mut ctx) {
334 Ok(()) => $crate::SUCCESS,
335 Err(error) => $crate::entrypoint::err_to_u64(error),
336 }
337 }
338 };
339}
340
341/// Backward-compatible alias for `hopper_lazy_entrypoint!`.
342#[macro_export]
343macro_rules! lazy_entrypoint {
344 ( $process:expr ) => {
345 $crate::hopper_lazy_entrypoint!($process);
346 };
347}
348
349/// Set up a no-op global allocator that aborts on allocation.
350///
351/// Useful for `no_std` programs that must not allocate. Any attempt to
352/// allocate immediately aborts the invocation through the SVM's abort syscall.
353/// No experimental inline assembly is required. Returning null would also be
354/// valid for `GlobalAlloc`; this allocator deliberately fails immediately.
355#[macro_export]
356macro_rules! no_allocator {
357 () => {
358 #[cfg(target_os = "solana")]
359 mod __hopper_allocator {
360 struct NoAlloc;
361
362 unsafe impl core::alloc::GlobalAlloc for NoAlloc {
363 unsafe fn alloc(&self, _layout: core::alloc::Layout) -> *mut u8 {
364 // SAFETY: abort accepts no pointers and never returns.
365 unsafe { $crate::syscalls::abort() }
366 }
367 unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {}
368 }
369
370 #[global_allocator]
371 static ALLOCATOR: NoAlloc = NoAlloc;
372 }
373 };
374}
375
376/// Canonical Solana heap region start address (`0x3_0000_0000`).
377pub const HEAP_START_ADDRESS: usize = 0x3_0000_0000;
378
379/// Default Solana heap region length (32 KiB).
380pub const HEAP_LENGTH: usize = 32 * 1024;
381
382/// Bytes of the heap's BOTTOM reserved as Hopper runtime scratch, starting
383/// right after the [`BumpAllocator`] cursor word: the byte range
384/// `[HEAP_START + 8, HEAP_START + 8 + HEAP_RUNTIME_RESERVED)`.
385///
386/// Why this exists: deployed SBF programs cannot carry writable sections,
387/// the loader rejects `.bss`/`.data` outright (`WritableSectionNotSupported`),
388/// so a `static mut` is not merely costly, it makes the program FAIL TO
389/// LOAD. The only writable, per-invocation, zero-initialized memory a
390/// program owns is this VM heap region. Hopper's instruction-scoped
391/// runtime state (today: the lamport gate in
392/// `hopper_runtime::write_policy`) therefore lives at the heap bottom,
393/// which works precisely because the VM zeroes the region on every
394/// invocation and every such structure is valid all-zero.
395///
396/// The [`BumpAllocator`] treats this range as out of bounds (its floor sits
397/// above it), so `alloc` can never hand it out. Programs that install a
398/// custom allocator over the heap must honor the same reservation if they
399/// link any hopper-runtime feature that uses it.
400pub const HEAP_RUNTIME_RESERVED: usize = 20 * 1024;
401
402/// Bytes at the top of the reserved scratch that hold the per-invocation
403/// Rent cache (`hopper_runtime::rent::live_rent`): the rate, the threshold
404/// bits, and a loaded flag. All-zero is the empty cache, so the VM's
405/// zeroed heap needs no initialization, exactly like the gate store below
406/// it. The gate store and the touch log assert that they end before
407/// [`RENT_CACHE_HEAP_OFFSET`].
408pub const RENT_CACHE_BYTES: usize = 32;
409
410/// Heap offset of the Rent cache, relative to [`HEAP_START_ADDRESS`].
411pub const RENT_CACHE_HEAP_OFFSET: usize = HEAP_RUNTIME_RESERVED - RENT_CACHE_BYTES;
412
413/// The largest heap a transaction can request with
414/// `ComputeBudgetInstruction::RequestHeapFrame` (256 KiB).
415pub const MAX_HEAP_LENGTH: usize = 256 * 1024;
416
417/// A bump allocator over the SVM heap region.
418///
419/// Single pass and never frees, like the Solana SDK's and pinocchio's: the
420/// first word of the heap holds the cursor and `dealloc` does nothing. It
421/// is the right allocator for the cold paths of a program that wants
422/// `alloc` (a `Vec` while building a CPI) while the hot path allocates
423/// nothing. For programs that must never allocate, prefer [`no_allocator!`]
424/// so a stray allocation traps.
425///
426/// # It grows upward, so a requested heap frame is usable
427///
428/// The cursor starts just above Hopper's reserved scratch and moves up. A
429/// program that declares a larger heap with `default_allocator!(heap = N)`
430/// can use all of it when the transaction carries
431/// `RequestHeapFrame(N)`. When the transaction does not, the first 32 KiB
432/// still work exactly as before, because small allocations land at the
433/// bottom either way; only an allocation that reaches past the memory the
434/// VM mapped faults, and the VM, not the allocator, stops it. An allocator
435/// that grows downward from the top of a 256 KiB region would fault on its
436/// first allocation in every transaction that forgot the request.
437///
438/// # The last allocation resizes in place
439///
440/// `realloc` of the most recent allocation moves the cursor and copies
441/// nothing. A `Vec` that grows while nothing else allocates, the usual
442/// case in a handler, costs its final size and not the sum of every size
443/// it passed through.
444///
445/// # Checkpoints
446///
447/// [`mark`](Self::mark) and [`release_to`](Self::release_to) give a loop
448/// the heap back on every iteration, which a bump allocator otherwise
449/// cannot do.
450///
451/// Install it with `default_allocator!`.
452pub struct BumpAllocator {
453 /// Heap region start address.
454 pub start: usize,
455 /// Heap region length in bytes.
456 pub len: usize,
457}
458
459/// A position of the heap cursor, taken with [`BumpAllocator::mark`].
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub struct HeapMark(usize);
462
463impl HeapMark {
464 /// The mark a host build hands out: there is no VM heap off chain.
465 #[cfg(not(target_os = "solana"))]
466 pub(crate) const fn host() -> Self {
467 Self(0)
468 }
469}
470
471impl BumpAllocator {
472 /// An allocator over the SVM heap, `len` bytes long. `len` is the heap
473 /// the program expects: 32 KiB by default, up to 256 KiB when its
474 /// transactions request a heap frame. Anything else is a compile
475 /// error in a `static`.
476 pub const fn new(len: usize) -> Self {
477 assert!(
478 len >= HEAP_LENGTH && len <= MAX_HEAP_LENGTH,
479 "the heap is between 32 KiB and 256 KiB"
480 );
481 assert!(
482 len.is_multiple_of(1024),
483 "a heap frame is a multiple of 1 KiB"
484 );
485 Self {
486 start: HEAP_START_ADDRESS,
487 len,
488 }
489 }
490
491 /// The lowest address the allocator hands out: above the cursor word
492 /// and the Hopper runtime scratch ([`HEAP_RUNTIME_RESERVED`]).
493 #[inline(always)]
494 const fn floor(&self) -> usize {
495 self.start + core::mem::size_of::<usize>() + HEAP_RUNTIME_RESERVED
496 }
497
498 #[inline(always)]
499 fn cursor(&self) -> usize {
500 // SAFETY: `start` is the heap's first word, which this allocator
501 // owns as its cursor; the VM zeroes the heap, and a program runs on
502 // one thread.
503 let pos = unsafe { *(self.start as *const usize) };
504 // Zero is the VM's fresh heap: nothing allocated yet.
505 if pos == 0 {
506 self.floor()
507 } else {
508 pos
509 }
510 }
511
512 #[inline(always)]
513 fn set_cursor(&self, pos: usize) {
514 // SAFETY: as in `cursor`; the word is written only here.
515 unsafe { *(self.start as *mut usize) = pos };
516 }
517
518 /// Bytes handed out so far, alignment padding included.
519 #[inline]
520 pub fn used(&self) -> usize {
521 self.cursor() - self.floor()
522 }
523
524 /// Bytes left before the declared end of the heap.
525 #[inline]
526 pub fn remaining(&self) -> usize {
527 (self.start + self.len).saturating_sub(self.cursor())
528 }
529
530 /// The current position of the heap, to return to with
531 /// [`release_to`](Self::release_to).
532 #[inline]
533 pub fn mark(&self) -> HeapMark {
534 HeapMark(self.cursor())
535 }
536
537 /// Give back every allocation made since `mark` was taken.
538 ///
539 /// # Safety
540 ///
541 /// Nothing allocated after `mark` may be used again: every `Box`,
542 /// `Vec`, and `String` created since must already be dropped or
543 /// forgotten. `mark` must come from this allocator in this invocation.
544 #[inline]
545 pub unsafe fn release_to(&self, mark: HeapMark) {
546 let pos = mark.0;
547 if pos >= self.floor() && pos <= self.cursor() {
548 self.set_cursor(pos);
549 }
550 }
551}
552
553// SAFETY: every pointer returned lies in `[floor, start + len)`, is aligned
554// as the layout asks, and is never returned twice while live: the cursor
555// only moves past what was handed out, except in `realloc` of the most
556// recent block (which stays where it is) and in `release_to` (whose caller
557// promises the released blocks are dead). A program runs on one thread, so
558// the cursor word is never accessed concurrently.
559unsafe impl core::alloc::GlobalAlloc for BumpAllocator {
560 // SAFETY: `GlobalAlloc::alloc` is `unsafe` by the trait's signature.
561 // The body is integer arithmetic on the cursor with every sum checked;
562 // it asks nothing of the caller beyond a valid `Layout`, whose
563 // alignment is a nonzero power of two by construction.
564 #[inline]
565 unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
566 let mask = layout.align() - 1;
567 let start = match self.cursor().checked_add(mask) {
568 Some(bumped) => bumped & !mask,
569 None => return core::ptr::null_mut(),
570 };
571 let end = match start.checked_add(layout.size()) {
572 Some(end) => end,
573 None => return core::ptr::null_mut(),
574 };
575 if end > self.start + self.len {
576 return core::ptr::null_mut();
577 }
578 self.set_cursor(end);
579 start as *mut u8
580 }
581
582 // SAFETY: `GlobalAlloc::dealloc` is `unsafe` by the trait's signature.
583 // This body reads and writes nothing, so it holds for any pointer and
584 // layout: a bump allocator reclaims its memory when the instruction
585 // ends.
586 #[inline]
587 unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {}
588
589 // SAFETY: `GlobalAlloc::realloc` is `unsafe` by the trait's signature.
590 // The caller vouches that `ptr` is a live block of this allocator with
591 // layout `layout`; the body moves the cursor or copies `layout.size()`
592 // bytes out of that block, and nothing else.
593 #[inline]
594 unsafe fn realloc(
595 &self,
596 ptr: *mut u8,
597 layout: core::alloc::Layout,
598 new_size: usize,
599 ) -> *mut u8 {
600 let block = ptr as usize;
601 // The most recent allocation ends at the cursor: it grows or
602 // shrinks where it is.
603 if block.checked_add(layout.size()) == Some(self.cursor()) {
604 return match block.checked_add(new_size) {
605 Some(end) if end <= self.start + self.len => {
606 self.set_cursor(end);
607 ptr
608 }
609 _ => core::ptr::null_mut(),
610 };
611 }
612 if new_size <= layout.size() {
613 // An older block that shrinks keeps its place.
614 return ptr;
615 }
616 // SAFETY: `new_size` is nonzero (it exceeds the old size) and the
617 // alignment is the old layout's, which the caller vouches for.
618 let new_layout =
619 unsafe { core::alloc::Layout::from_size_align_unchecked(new_size, layout.align()) };
620 // SAFETY: forwarded `GlobalAlloc` contract.
621 let new_ptr = unsafe { self.alloc(new_layout) };
622 if !new_ptr.is_null() {
623 // SAFETY: the old block is valid for `layout.size()` bytes, the
624 // new one for more, and a fresh block cannot overlap a live one.
625 unsafe { core::ptr::copy_nonoverlapping(ptr, new_ptr, layout.size()) };
626 }
627 new_ptr
628 }
629}
630
631/// Install the default bump allocator over the SVM heap region.
632///
633/// Opt-in counterpart to [`no_allocator!`]: use this when a program needs
634/// `alloc` (e.g. heap `Vec`/`String` on a cold path) while keeping the
635/// zero-copy hot path allocation-free. Never frees within an instruction;
636/// the whole heap is reclaimed when the instruction returns.
637///
638/// `default_allocator!(heap = 128 * 1024)` declares a larger heap, up to
639/// 256 KiB. The transactions that need it carry
640/// `ComputeBudgetInstruction::RequestHeapFrame` with the same size; the
641/// ones that allocate less than 32 KiB work without it.
642#[macro_export]
643macro_rules! default_allocator {
644 () => {
645 $crate::default_allocator!(heap = $crate::HEAP_LENGTH);
646 };
647 (heap = $len:expr) => {
648 #[cfg(target_os = "solana")]
649 #[global_allocator]
650 static ALLOCATOR: $crate::BumpAllocator = $crate::BumpAllocator::new($len);
651 };
652}
653
654/// Whether a panic reports where it happened (`panic-location`).
655pub const PANIC_REPORTS_LOCATION: bool = cfg!(feature = "panic-location");
656/// Whether a panic logs its message (`panic-message`).
657pub const PANIC_REPORTS_MESSAGE: bool = cfg!(feature = "panic-message");
658
659/// What the panic handler does, as a function so the features that shape
660/// it are this crate's and not the calling program's.
661///
662/// - By default: abort. Nothing is logged and no formatting code is
663/// linked, so a release build pays nothing for having a handler.
664/// - With `panic-location`: the runtime logs `panicked at file:line:col`
665/// (through `sol_panic_`) before the transaction fails. The file names
666/// are the only cost, a few hundred bytes of read-only data.
667/// - With `panic-message`: the panic's message is logged first, cut at
668/// the last whole character that fits 256 bytes. This links the
669/// formatting machinery, so it is for debugging builds.
670///
671/// Turn the features on through `hopper` (`features = ["panic-location"]`)
672/// while a program is being debugged, and off again for the build that
673/// ships.
674#[cfg(target_os = "solana")]
675#[inline(always)]
676pub fn report_panic(info: &core::panic::PanicInfo<'_>) -> ! {
677 #[cfg(feature = "panic-message")]
678 {
679 use core::fmt::Write;
680 let mut buf = [0u8; 256];
681 let mut writer = crate::log::StackWriter::new(&mut buf);
682 let _ = write!(writer, "{}", info.message());
683 crate::log::log(writer.as_str());
684 }
685 #[cfg(feature = "panic-location")]
686 if let Some(location) = info.location() {
687 let file = location.file();
688 // SAFETY: the pointer and the length describe one live `&str`;
689 // the syscall logs it with the line and column and never returns.
690 unsafe {
691 crate::syscalls::sol_panic_(
692 file.as_ptr(),
693 file.len() as u64,
694 location.line() as u64,
695 location.column() as u64,
696 )
697 }
698 }
699 let _ = info;
700 // SAFETY: abort accepts no pointers and never returns.
701 unsafe { crate::syscalls::abort() }
702}
703
704/// The `no_std` panic handler.
705///
706/// Aborts through the SVM's abort syscall, without experimental inline
707/// assembly or a compute-consuming spin loop; the runtime rolls the
708/// instruction back. With the `panic-location` feature it first reports
709/// the file, line, and column, and with `panic-message` the message. See
710/// [`report_panic`](crate::entrypoint::report_panic).
711#[macro_export]
712macro_rules! nostd_panic_handler {
713 () => {
714 #[cfg(target_os = "solana")]
715 #[panic_handler]
716 fn panic(info: &core::panic::PanicInfo) -> ! {
717 $crate::entrypoint::report_panic(info)
718 }
719 };
720}
721
722#[cfg(test)]
723mod entrypoint_tail_tests {
724 extern crate std;
725
726 use std::vec;
727 use std::vec::Vec;
728
729 use super::*;
730
731 /// Serialize a zero-account loader frame: `u64` account count (0), then the
732 /// `u64` ix-data length prefix, the ix-data bytes, and the 32-byte program
733 /// id. Returns an 8-aligned `u64` backing (matching `MM_INPUT_START`).
734 fn build_zero_account_frame(ix_data: &[u8], program_id: [u8; 32]) -> Vec<u64> {
735 let mut buf: Vec<u8> = Vec::new();
736 buf.extend_from_slice(&0u64.to_le_bytes()); // account_count = 0
737 buf.extend_from_slice(&(ix_data.len() as u64).to_le_bytes());
738 buf.extend_from_slice(ix_data);
739 buf.extend_from_slice(&program_id);
740 let mut words = vec![0u64; buf.len().div_ceil(8)];
741 // SAFETY: `words` has at least `buf.len()` bytes of capacity and the
742 // regions do not overlap.
743 unsafe {
744 core::ptr::copy_nonoverlapping(buf.as_ptr(), words.as_mut_ptr() as *mut u8, buf.len());
745 }
746 words
747 }
748
749 fn ok_handler<'a>(
750 _: &'a Address,
751 _: &'a [AccountView<'a>],
752 _: &'a [u8],
753 ) -> crate::ProgramResult {
754 Ok(())
755 }
756
757 fn custom_err_handler<'a>(
758 _: &'a Address,
759 _: &'a [AccountView<'a>],
760 _: &'a [u8],
761 ) -> crate::ProgramResult {
762 Err(ProgramError::Custom(4242))
763 }
764
765 fn builtin_err_handler<'a>(
766 _: &'a Address,
767 _: &'a [AccountView<'a>],
768 _: &'a [u8],
769 ) -> crate::ProgramResult {
770 Err(ProgramError::MissingRequiredSignature)
771 }
772
773 #[test]
774 fn ok_returns_bare_success_zero() {
775 let mut frame = build_zero_account_frame(&[1, 2, 3], [7u8; 32]);
776 // SAFETY: `frame` is a well-formed, 8-aligned zero-account loader frame.
777 let code = unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, ok_handler) };
778 assert_eq!(code, 0);
779 assert_eq!(code, crate::SUCCESS);
780 }
781
782 #[test]
783 fn custom_err_maps_through_cold_outline_unchanged() {
784 let mut frame = build_zero_account_frame(&[], [0u8; 32]);
785 // SAFETY: well-formed, 8-aligned zero-account loader frame.
786 let code =
787 unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, custom_err_handler) };
788 // Cold outline must equal the direct `From<ProgramError> for u64` mapping.
789 assert_eq!(code, u64::from(ProgramError::Custom(4242)));
790 assert_eq!(code, err_to_u64(ProgramError::Custom(4242)));
791 assert_eq!(code, 4242);
792 }
793
794 #[test]
795 fn builtin_err_maps_through_cold_outline_unchanged() {
796 let mut frame = build_zero_account_frame(&[], [0u8; 32]);
797 // SAFETY: well-formed, 8-aligned zero-account loader frame.
798 let code =
799 unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, builtin_err_handler) };
800 assert_eq!(code, u64::from(ProgramError::MissingRequiredSignature));
801 assert_eq!(code, err_to_u64(ProgramError::MissingRequiredSignature));
802 }
803
804 /// The cold outline is a byte-for-byte alias of `From<ProgramError> for u64`
805 /// across the full variant space (custom-zero, custom, and builtins).
806 #[test]
807 fn err_to_u64_matches_from_impl_for_all_variants() {
808 let cases = [
809 ProgramError::Custom(0),
810 ProgramError::Custom(1),
811 ProgramError::Custom(u32::MAX),
812 ProgramError::InvalidArgument,
813 ProgramError::MissingRequiredSignature,
814 ProgramError::AccountBorrowFailed,
815 ProgramError::ArithmeticOverflow,
816 ProgramError::IncorrectAuthority,
817 ];
818 for e in cases {
819 assert_eq!(err_to_u64(e.clone()), u64::from(e));
820 }
821 }
822}
823
824#[cfg(test)]
825mod allocator_tests {
826 extern crate std;
827
828 use super::*;
829 use core::alloc::{GlobalAlloc, Layout};
830 use std::vec;
831 use std::vec::Vec;
832
833 /// A zeroed, 8-aligned region that stands in for the VM's heap, and an
834 /// allocator over it.
835 fn test_heap(len: usize) -> (Vec<u64>, BumpAllocator) {
836 let mut backing = vec![0u64; len / 8];
837 let allocator = BumpAllocator {
838 start: backing.as_mut_ptr() as usize,
839 len,
840 };
841 (backing, allocator)
842 }
843
844 const SCRATCH_END: usize = 8 + HEAP_RUNTIME_RESERVED;
845
846 fn layout(size: usize, align: usize) -> Layout {
847 Layout::from_size_align(size, align).unwrap()
848 }
849
850 #[test]
851 fn dealloc_gives_nothing_back_and_touches_nothing() {
852 let (backing, heap) = test_heap(HEAP_LENGTH);
853 // SAFETY: a test allocation from a private region.
854 let a = unsafe { heap.alloc(layout(64, 8)) };
855 let used = heap.used();
856 // SAFETY: `a` came from this allocator with this layout.
857 unsafe { heap.dealloc(a, layout(64, 8)) };
858 assert_eq!(heap.used(), used);
859 // SAFETY: as above.
860 let b = unsafe { heap.alloc(layout(64, 8)) };
861 assert_eq!(b as usize, a as usize + 64, "the freed block is not reused");
862 // Only the cursor word was ever written.
863 assert!(backing[1..].iter().all(|word| *word == 0));
864 }
865
866 #[test]
867 fn allocations_start_above_the_scratch_and_grow_upward() {
868 let (backing, heap) = test_heap(HEAP_LENGTH);
869 assert_eq!(heap.used(), 0);
870 assert_eq!(heap.remaining(), HEAP_LENGTH - SCRATCH_END);
871 // SAFETY: a test allocation from a private region, never freed.
872 let a = unsafe { heap.alloc(layout(10, 1)) } as usize;
873 // SAFETY: as above.
874 let b = unsafe { heap.alloc(layout(8, 8)) } as usize;
875 // SAFETY: as above.
876 let c = unsafe { heap.alloc(layout(1, 1)) } as usize;
877 assert_eq!(a, heap.start + SCRATCH_END);
878 assert_eq!(
879 b,
880 heap.start + SCRATCH_END + 16,
881 "aligned up past the 10 bytes"
882 );
883 assert_eq!(c, b + 8);
884 assert_eq!(heap.used(), 25);
885 // The scratch region was not written.
886 assert!(backing[1..SCRATCH_END / 8].iter().all(|w| *w == 0));
887 }
888
889 #[test]
890 fn every_alignment_is_honoured() {
891 let (_backing, heap) = test_heap(HEAP_LENGTH);
892 for shift in 0..8 {
893 let align = 1usize << shift;
894 // SAFETY: a test allocation from a private region.
895 let odd = unsafe { heap.alloc(layout(3, 1)) };
896 assert!(!odd.is_null());
897 // SAFETY: as above.
898 let ptr = unsafe { heap.alloc(layout(5, align)) } as usize;
899 assert_eq!(ptr % align, 0, "alignment {align}");
900 }
901 }
902
903 #[test]
904 fn the_heap_is_exhausted_at_its_declared_end_and_not_before() {
905 let (_backing, heap) = test_heap(HEAP_LENGTH);
906 let room = HEAP_LENGTH - SCRATCH_END;
907 // SAFETY: a test allocation from a private region.
908 let all = unsafe { heap.alloc(layout(room, 1)) };
909 assert!(!all.is_null());
910 assert_eq!(heap.remaining(), 0);
911 // SAFETY: as above.
912 assert!(unsafe { heap.alloc(layout(1, 1)) }.is_null());
913 // A refused allocation leaves the cursor where it was.
914 assert_eq!(heap.used(), room);
915
916 let (_backing, heap) = test_heap(HEAP_LENGTH);
917 // SAFETY: as above.
918 assert!(unsafe { heap.alloc(layout(room + 1, 1)) }.is_null());
919 assert_eq!(heap.used(), 0);
920 // A size that would wrap the address space is refused too.
921 // SAFETY: as above.
922 assert!(unsafe { heap.alloc(layout(isize::MAX as usize - 64, 1)) }.is_null());
923 }
924
925 #[test]
926 fn a_requested_heap_frame_is_usable_to_its_end() {
927 let (_backing, heap) = test_heap(MAX_HEAP_LENGTH);
928 // More than the default heap holds in one allocation.
929 // SAFETY: a test allocation from a private region.
930 let big = unsafe { heap.alloc(layout(200 * 1024, 8)) };
931 assert!(!big.is_null());
932 // SAFETY: the block is 200 KiB of the private region.
933 unsafe { core::ptr::write_bytes(big, 0xA5, 200 * 1024) };
934 assert_eq!(heap.remaining(), MAX_HEAP_LENGTH - SCRATCH_END - 200 * 1024);
935 // A small allocation made first lands in the first 32 KiB, which
936 // every transaction has.
937 let (_backing, heap) = test_heap(MAX_HEAP_LENGTH);
938 // SAFETY: as above.
939 let small = unsafe { heap.alloc(layout(64, 8)) } as usize;
940 assert!(small + 64 <= heap.start + HEAP_LENGTH);
941 }
942
943 #[test]
944 fn the_last_allocation_resizes_in_place() {
945 let (_backing, heap) = test_heap(HEAP_LENGTH);
946 let first = layout(16, 8);
947 // SAFETY: test allocations from a private region; each block is
948 // used only within its size.
949 unsafe {
950 let ptr = heap.alloc(first);
951 core::ptr::write_bytes(ptr, 7, 16);
952 let grown = heap.realloc(ptr, first, 64);
953 assert_eq!(grown, ptr, "grown where it is");
954 assert_eq!(heap.used(), 64);
955 assert_eq!(*grown.add(15), 7);
956 let shrunk = heap.realloc(grown, layout(64, 8), 8);
957 assert_eq!(shrunk, ptr);
958 assert_eq!(heap.used(), 8, "the tail was given back");
959
960 // Growing past the end is refused and changes nothing.
961 let refused = heap.realloc(shrunk, layout(8, 8), HEAP_LENGTH);
962 assert!(refused.is_null());
963 assert_eq!(heap.used(), 8);
964 }
965 }
966
967 #[test]
968 fn an_older_allocation_moves_when_it_grows() {
969 let (_backing, heap) = test_heap(HEAP_LENGTH);
970 let old = layout(8, 8);
971 // SAFETY: test allocations from a private region; each block is
972 // used only within its size.
973 unsafe {
974 let a = heap.alloc(old);
975 core::ptr::write_bytes(a, 0x11, 8);
976 let b = heap.alloc(layout(8, 8));
977 core::ptr::write_bytes(b, 0x22, 8);
978 let moved = heap.realloc(a, old, 24);
979 assert!(
980 moved as usize > b as usize,
981 "a new block above the newer one"
982 );
983 assert_eq!(core::slice::from_raw_parts(moved, 8), &[0x11; 8]);
984 assert_eq!(
985 core::slice::from_raw_parts(b, 8),
986 &[0x22; 8],
987 "the neighbour is intact"
988 );
989 // Shrinking an older block keeps it in place.
990 assert_eq!(heap.realloc(b, layout(8, 8), 4), b);
991 }
992 }
993
994 #[test]
995 fn a_checkpoint_gives_the_heap_back() {
996 let (_backing, heap) = test_heap(HEAP_LENGTH);
997 // SAFETY: test allocations from a private region; nothing
998 // allocated after the mark is used after the release.
999 unsafe {
1000 let kept = heap.alloc(layout(32, 8));
1001 let mark = heap.mark();
1002 let mut previous: *mut u8 = core::ptr::null_mut();
1003 for _ in 0..1_000 {
1004 let scratch = heap.alloc(layout(4096, 8));
1005 assert!(!scratch.is_null(), "the loop never runs out");
1006 if !previous.is_null() {
1007 assert_eq!(scratch, previous, "the same bytes every time");
1008 }
1009 previous = scratch;
1010 heap.release_to(mark);
1011 }
1012 assert_eq!(heap.used(), 32);
1013 assert_eq!(heap.mark(), mark);
1014 assert!(!kept.is_null());
1015 // A mark from above the cursor or below the floor is ignored.
1016 let after = heap.alloc(layout(8, 8));
1017 let high = heap.mark();
1018 heap.release_to(mark);
1019 heap.release_to(high);
1020 assert_eq!(heap.used(), 32, "a stale mark cannot move the cursor up");
1021 assert!(!after.is_null());
1022 }
1023 }
1024
1025 #[test]
1026 fn the_declared_heap_is_checked() {
1027 assert_eq!(BumpAllocator::new(HEAP_LENGTH).len, HEAP_LENGTH);
1028 assert_eq!(
1029 BumpAllocator::new(MAX_HEAP_LENGTH).start,
1030 HEAP_START_ADDRESS
1031 );
1032 for bad in [
1033 0,
1034 HEAP_LENGTH - 1024,
1035 HEAP_LENGTH + 1,
1036 MAX_HEAP_LENGTH + 1024,
1037 ] {
1038 assert!(
1039 std::panic::catch_unwind(|| BumpAllocator::new(bad)).is_err(),
1040 "{bad}"
1041 );
1042 }
1043 }
1044}