CPI Execution and Privileges

Summary

CPIs pass through 11 runtime steps including privilege checking, account translation, and data sync. Max call depth: 5 (9 with SIMD-0268). Privilege rules prevent callee from escalating beyond what the caller granted.

Privilege rules

CPIs extend the caller's account privileges to the callee with strict enforcement. The runtime checks these rules in prepare_next_instruction:

ScenarioAllowed?Enforcement pointError
Caller passes account as writable, callee marks writableYes----
Caller passes account as read-only, callee marks writableNoprepare_next_instructionPrivilegeEscalation
Caller passes account as writable, callee marks read-onlyYes----
Caller passes account as signer, callee marks signerYes----
Caller passes account as non-signer, callee marks signer, account is a PDA derived from caller's seedsYesprepare_next_instruction--
Caller passes account as non-signer, callee marks signer, account is NOT a PDA from callerNoprepare_next_instructionPrivilegeEscalation
Caller passes account as signer, callee marks non-signerYes----
Program A calls itself directly (A -> A)Yespush()--
Program A calls B which calls A (indirect reentrancy)Nopush()ReentrancyNotAllowed
CPI to native loader, bpf_loader, bpf_loader_deprecated, or precompileNocheck_authorized_programProgramNotSupported
Account not found in transactionNoprepare_next_instructionMissingAccount

The privilege rules can be summarized as:

  1. Writable privilege cannot escalate. If the caller marks an account as read-only, the callee cannot mark it writable.
  2. Signer privilege requires authorization. An account can be a signer in the callee only if (a) it was already a signer in the caller, OR (b) it is a PDA derived from the calling program's seeds via invoke_signed.
  3. Privilege reduction is always allowed. The callee may use fewer privileges than the caller granted.

CPI execution flow

A CPI passes through several runtime layers. This section documents the full pipeline from the program SDK call through the syscall boundary into the runtime and back. Each step references the source file that implements it.

The maximum height of the program instruction invocation is called the max_instruction_stack_depth and is set to the MAX_INSTRUCTION_STACK_DEPTH constant of 5. With MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 active, this increases to 9.

Stack height 1 is the initial transaction instruction. Each CPI increments the height by 1. A maximum of 5 means a program can make CPIs up to 4 levels deep (8 levels deep with SIMD-0268).

Step 1: Program calls invoke or invoke_signed

The program calls invoke or invoke_signed. invoke is a thin wrapper that calls invoke_signed with an empty signer seeds array. The SDK function serializes the Instruction, AccountInfo slice, and signer seeds into VM memory, then triggers the syscall.

Step 2: Syscall entry

The SBF VM dispatches to the sol_invoke_signed_rust syscall handler, which calls into the shared entry point: cpi_common.

Step 3: Consume invocation cost

The first action inside cpi_common is to charge the fixed invocation cost from the shared compute meter: invoke_units = 1,000 CUs (or 946 CUs with SIMD-0339).

Step 4: Translate instruction from VM memory

The syscall handler translates the instruction from the program's VM address space to host-side Rust types via translate_instruction_rust, which reads a StableInstruction struct, validates data length against MAX_INSTRUCTION_DATA_LEN (10,240 bytes), then charges the data serialization cost.

Step 5: Translate signer seeds and derive PDAs

The handler calls translate_signers_rust. For each set of signer seeds, the runtime:

  1. Checks the number of signer seed sets against MAX_SIGNERS (16).
  2. Checks each seed set's length against MAX_SEEDS (16 seeds per set).
  3. Calls Pubkey::create_program_address with the seeds and the caller's program ID. If the seeds do not produce a valid PDA, the CPI fails with BadSeeds.
  4. Collects the resulting PDA pubkeys into a signers vec.

These derived PDAs are treated as valid signers for the callee instruction.

Step 6: Check authorized program

Before proceeding, the runtime calls check_authorized_program to verify the target program is allowed for CPI. The following programs are blocked:

  • The native loader
  • bpf_loader and bpf_loader_deprecated
  • bpf_loader_upgradeable (except specific management instructions: upgrade, set_authority, set_authority_checked (feature-gated), extend_program_checked (feature-gated), close)
  • Precompile programs (ed25519, secp256k1, etc.)

Violation returns ProgramNotSupported.

Step 7: Privilege verification (prepare_next_instruction)

The runtime calls prepare_next_instruction which builds the callee's InstructionAccount list and enforces privilege rules. See Privilege rules below for the full decision table.

Step 8: Translate account infos

The handler calls translate_accounts which:

  1. Validates the account info count against MAX_CPI_ACCOUNT_INFOS (128, or 255 with SIMD-0339).
  2. Charges account info translation cost (SIMD-0339 only): (num_account_infos * 80) / 250 CUs.
  3. For each non-executable, non-duplicate account, builds a CallerAccount by translating pointers from VM memory to host memory. This includes charging per-account data serialization cost: account_data_len / cpi_bytes_per_unit CUs.

Step 9: Pre-CPI account sync (caller to callee)

Before executing the callee, the runtime syncs the caller's account modifications so the callee can see them. The function update_callee_account is called for each translated account, copying lamports, data, and owner. See Account data synchronization for the detailed field mapping.

Step 10: Push instruction context, execute callee, and pop

The runtime calls process_instruction, which:

  1. Calls push() to add a new frame to the instruction stack. push() enforces the reentrancy rule: a program may only call itself if it is the direct caller (i.e., program A can call A, but A cannot call B which calls A). Violation returns ReentrancyNotAllowed.
  2. Calls process_executable_chain which resolves the callee's program entry point and invokes it. The callee runs with the same shared compute meter. All CU consumption by the callee reduces the caller's remaining budget.
  3. Calls pop() to remove the callee's frame and verify that lamport balances are unchanged (UnbalancedInstruction if not).

Step 11: Post-CPI account sync (callee to caller)

After process_instruction returns (which includes the pop), the runtime syncs changes back to the caller via update_caller_account for each writable account. Additionally, update_caller_account_region updates VM memory region mappings for accounts whose data regions changed. See Account data synchronization for the detailed field mapping.

The CPI syscall returns 0 (success) to the caller program.

Is this page helpful?

द्वारा प्रबंधित

© 2026 सोलाना फाउंडेशन। सर्वाधिकार सुरक्षित।
जुड़े रहें
CPI Execution and Privileges | Solana