CPI Cycle Detection
Detects circular cross-program invocation patterns that could cause infinite loops or resource exhaustion.
CPI Cycle Detection
Overview
Remediation Guide: How to Fix CPI Cycles
The CPI cycle detector identifies circular cross-program invocation patterns that can cause infinite loops or exhaust Solana’s CPI depth limit of 4. It detects three patterns: self-referential CPI calls (a program invoking itself), CPI chains that exceed the depth limit, and CPI patterns that suggest a function could participate in a larger cross-program cycle.
Why This Is an Issue
Solana enforces a maximum CPI depth of 4. If programs form a cycle (A calls B, B calls A), or a program calls itself, the runtime will abort with a depth limit exceeded error. Even without reaching the limit, deep CPI chains consume significant compute units and can be exploited for denial of service. An attacker who can trigger a CPI cycle can cause transactions to fail, potentially locking funds in contracts that depend on successful execution.
CWE mapping: CWE-674 (Uncontrolled Recursion).
How to Resolve
Native Solana
pub fn process(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
let target_program = &accounts[3];
// Prevent self-invocation
if target_program.key == program_id {
return Err(ProgramError::InvalidArgument);
}
invoke(&ix, accounts)?;
Ok(())
}
Anchor
pub fn process(ctx: Context<Process>) -> Result<()> {
// Validate target is not self
require!(
ctx.accounts.target_program.key() != crate::ID,
ErrorCode::SelfInvocation
);
// CPI proceeds safely
Ok(())
}
Examples
Vulnerable Code
pub fn proxy_call(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let target = &accounts[2]; // Could be this program!
let ix = Instruction { program_id: *target.key, accounts: /* ... */, data: data.to_vec() };
invoke(&ix, accounts)?; // If target == self, infinite recursion
Ok(())
}
Fixed Code
pub fn proxy_call(accounts: &[AccountInfo], program_id: &Pubkey, data: &[u8]) -> ProgramResult {
let target = &accounts[2];
if target.key == program_id {
return Err(ProgramError::InvalidArgument);
}
let ix = Instruction { program_id: *target.key, accounts: /* ... */, data: data.to_vec() };
invoke(&ix, accounts)?;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "cpi-cycle",
"severity": "high",
"confidence": 0.78,
"description": "Function 'proxy_call' makes a CPI call that could target itself, creating a direct recursion loop.",
"location": { "function": "proxy_call", "block": 0, "stmt": 1 }
}
Detection Methodology
- CPI graph construction: Builds a graph of CPI targets within each function, tracking program variable IDs.
- Self-call detection: Flags CPI calls in functions whose names suggest CPI operations (containing “cpi” or “invoke”) with unvalidated targets.
- Depth analysis: Counts CPI calls per function; functions with 4 or more CPIs are flagged for exceeding the safe depth.
- Parameter analysis: CPI targets from function parameters (external input) without visible validation are flagged as potential cycle participants.
Limitations
- The detector operates within a single function and cannot trace CPI cycles across multiple programs.
- Functions with validated CPI targets that happen to match naming heuristics may produce false positives.
- The depth analysis counts CPIs within a function, not actual runtime call depth across programs.
Related Detectors
- CPI in Loop DOS — detects CPI in loops causing compute exhaustion
- CPI Reentrancy — detects re-entrant CPI patterns