Skip to main content
Sigvex

Account Owner Chain Remediation

How to fix stale owner validation across CPI boundaries.

Remediating Account Owner Chain Issues

Overview

Related Detector: Account Owner Chain

After a cross-program invocation, any pre-CPI owner validation is stale because the invoked program may have modified the account. The fix is to re-validate account ownership after every CPI before accessing the account again.

Before (Vulnerable)

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let account = &accounts[0];

    // Owner check
    if account.owner != &MY_PROGRAM_ID {
        return Err(ProgramError::IllegalOwner);
    }

    // CPI
    solana_program::program::invoke(&instruction, &[account.clone()])?;

    // Access without re-validation -- VULNERABLE
    let data = account.data.borrow();
    Ok(())
}

After (Fixed)

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let account = &accounts[0];

    if account.owner != &MY_PROGRAM_ID {
        return Err(ProgramError::IllegalOwner);
    }

    solana_program::program::invoke(&instruction, &[account.clone()])?;

    // Re-validate after CPI
    if account.owner != &MY_PROGRAM_ID {
        return Err(ProgramError::IllegalOwner);
    }

    let data = account.data.borrow();
    Ok(())
}

Alternative Mitigations

  1. Access before CPI only: If possible, read all necessary data from the account before the CPI and do not access it afterward. This avoids the TOCTOU window entirely.
let data = account.data.borrow().to_vec(); // Read before CPI
drop(account.data.borrow());
solana_program::program::invoke(&instruction, &[account.clone()])?;
// Use 'data' snapshot -- no post-CPI access needed
  1. Use read-only accounts in CPI: If the invoked program does not need to modify the account, pass it as non-writable. This prevents ownership changes during the CPI.

  2. Validate multiple properties after CPI: Re-check not just ownership but also discriminator, data length, and key to ensure the account has not been tampered with.

// Full post-CPI validation
if account.owner != &MY_PROGRAM_ID {
    return Err(ProgramError::IllegalOwner);
}
if account.data.borrow()[0..8] != EXPECTED_DISCRIMINATOR {
    return Err(ErrorCode::InvalidDiscriminator.into());
}

Common Mistakes

  • Re-validating only one account when multiple are passed to CPI: All accounts that are accessed after a CPI must be re-validated, not just the primary account.
  • Re-validating against a variable that could have been modified: Re-check against a constant or a trusted PDA, not against a value read from another account that was also passed to the CPI.
  • Forgetting re-validation between consecutive CPIs: Each CPI creates a new trust boundary. If you make two CPIs, re-validate between them.
  • Assuming an account-validation framework handles post-CPI validation: Anchor validates accounts at instruction entry but does not automatically re-validate after CPI calls within the handler. You must add explicit checks.

References