Account Owner Chain
Detects account owner validation issues across CPI boundaries where ownership assumptions break after cross-program invocations.
Account Owner Chain
Overview
Remediation Guide: How to Fix Account Owner Chain Issues
The account owner chain detector identifies Solana programs where account ownership is validated before a CPI but not re-validated afterward. During a cross-program invocation, the invoked program can modify the account — closing it, transferring ownership, or changing its data. If the calling program accesses the account after the CPI without re-checking ownership, it may operate on an account that no longer satisfies its original security assumptions.
Sigvex tracks owner checks (CheckOwner), CPI calls (InvokeCpi), and account accesses (AccountData, AccountOwner, AccountLamports, StoreAccountData) with precise position tracking. The detector reports accounts that are accessed after a CPI without an intervening owner re-validation. CWE mapping: CWE-367 (Time-of-check Time-of-use / TOCTOU).
Why This Is an Issue
Every CPI creates a trust boundary. During a CPI, the invoked program has full control over the accounts passed to it and can:
- Close the account: Zero lamports and reassign ownership to the system program.
- Transfer ownership: Assign the account to a different program.
- Modify data: Change account state in ways that violate the caller’s invariants.
After the CPI returns, the caller’s original owner check is stale. Accessing the account without re-validation can lead to:
- Privilege escalation: The account may now be owned by a malicious program that returns crafted data.
- Use-after-close: The account may have been closed, and reading its data yields zeroes or garbage.
- State confusion: The account’s data layout may have changed if ownership transferred to a program with a different schema.
How to Resolve
Native Rust
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
const EXPECTED_OWNER: Pubkey = /* expected program */;
pub fn process_with_cpi(accounts: &[AccountInfo]) -> ProgramResult {
let target = &accounts[0];
// Pre-CPI owner check
if target.owner != &EXPECTED_OWNER {
return Err(ProgramError::IllegalOwner);
}
// CPI call
solana_program::program::invoke(
&build_instruction(),
&[target.clone()],
)?;
// Re-validate after CPI
if target.owner != &EXPECTED_OWNER {
return Err(ProgramError::IllegalOwner);
}
// Now safe to access
let data = target.data.borrow();
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct ProcessWithCpi<'info> {
#[account(mut, owner = expected_program @ ErrorCode::InvalidOwner)]
pub target: AccountInfo<'info>,
}
// Re-validate after CPI in the instruction handler
pub fn handler(ctx: Context<ProcessWithCpi>) -> Result<()> {
// CPI call
invoke_external(&ctx)?;
// Re-validate ownership after CPI
require!(
ctx.accounts.target.owner == &EXPECTED_PROGRAM,
ErrorCode::InvalidOwner
);
Ok(())
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn compound_operation(accounts: &[AccountInfo]) -> ProgramResult {
let vault = &accounts[0];
let external_program = &accounts[1];
// Check owner before CPI
if vault.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
// CPI -- external program may close or reassign vault
solana_program::program::invoke(
&build_instruction(vault.key),
&[vault.clone(), external_program.clone()],
)?;
// BUG: Access vault without re-checking owner
let data = vault.data.borrow();
let balance = u64::from_le_bytes(data[0..8].try_into()?);
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn compound_operation(accounts: &[AccountInfo]) -> ProgramResult {
let vault = &accounts[0];
let external_program = &accounts[1];
if vault.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
solana_program::program::invoke(
&build_instruction(vault.key),
&[vault.clone(), external_program.clone()],
)?;
// Re-validate owner after CPI
if vault.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
let data = vault.data.borrow();
let balance = u64::from_le_bytes(data[0..8].try_into()?);
Ok(())
}
Sample Sigvex Output
{
"detector_id": "account-owner-chain",
"severity": "high",
"confidence": 0.80,
"description": "An account's owner is checked, then a CPI is invoked, and the account is accessed again without re-validating the owner. The original owner check is no longer valid after the CPI boundary.",
"location": { "function": "compound_operation", "offset": 3 }
}
Detection Methodology
The detector performs a multi-pass analysis:
- Operation collection: Collects all owner checks (with account variable and position), CPI calls (with position), and account accesses (reads and writes, with account variable and position).
- TOCTOU detection: For each owner-checked account, checks whether a CPI occurs after the check and an account access occurs after the CPI, without an intervening re-validation.
- Callback detection: Functions with callback-like names (
on_,handle_,process_cpi) that access accounts without any owner checks are flagged separately. - Consecutive CPI detection: Multiple sequential CPIs without intermediate owner validation are flagged.
Context modifiers:
- Anchor programs: confidence reduced by 60% (Anchor validates owner chains at deserialization)
- Admin/initialization functions: confidence reduced by 40%
- Read-only/view functions: confidence reduced by 60%
Limitations
False positives:
- Programs where the CPI target is a trusted, immutable program that cannot modify the account may be flagged unnecessarily.
- Callback-like function names that are not actually CPI callbacks (e.g.,
handle_input) may trigger the callback detection heuristic.
False negatives:
- Account accesses through indirect references (stored in a struct field rather than a direct variable) may not be tracked.
- Owner re-validation performed by a helper function called between the CPI and the access is not recognized.
Related Detectors
- Account Verification Chain — detects incomplete verification chains for individual accounts
- Missing Owner Check — detects accounts used without any owner validation