Skip to main content
Sigvex

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

  1. CPI graph construction: Builds a graph of CPI targets within each function, tracking program variable IDs.
  2. Self-call detection: Flags CPI calls in functions whose names suggest CPI operations (containing “cpi” or “invoke”) with unvalidated targets.
  3. Depth analysis: Counts CPI calls per function; functions with 4 or more CPIs are flagged for exceeding the safe depth.
  4. 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.

References