Account Use After Close
Detects reads or writes to accounts after they are logically closed.
Account Use After Close
Overview
Remediation Guide: How to Fix Account Use After Close
The account use after close detector identifies Solana programs that read from, write to, or pass accounts in CPI calls after the account has been logically closed. An account is considered closed when its lamports are fully drained or its discriminator data is zeroed. Using a closed account is analogous to a use-after-free vulnerability in systems programming and can lead to state corruption, unauthorized access, or fund loss.
Sigvex tracks close operations (lamport drain and data zeroing at offset 0) and subsequent account accesses within the same function, reporting any access that occurs after the close point.
Why This Is an Issue
- State corruption: writing to a closed account modifies data that should no longer exist, potentially confusing other instructions in the same transaction.
- Stale reads: reading from a closed account returns data from an account with zero lamports, which may violate assumptions about account validity.
- CPI hazards: passing a closed account to a CPI can cause the called program to operate on invalid state.
CWE mapping: CWE-416 (Use After Free).
How to Resolve
Native Solana
pub fn close_and_process(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
let destination = &accounts[1];
// Process BEFORE closing
let data = account.data.borrow();
let balance = u64::from_le_bytes(data[8..16].try_into()?);
process_balance(balance)?;
drop(data);
// Close LAST
account.data.borrow_mut().fill(0);
**destination.try_borrow_mut_lamports()? += account.lamports();
**account.try_borrow_mut_lamports()? = 0;
Ok(())
}
Anchor
// Anchor's close constraint ensures the account is closed at the end
#[derive(Accounts)]
pub struct Process<'info> {
#[account(mut, close = destination)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
}
Examples
Vulnerable
// Close then read — use after close
**destination.try_borrow_mut_lamports()? += account.lamports();
**account.try_borrow_mut_lamports()? = 0;
let data = account.data.borrow(); // Reading closed account!
Fixed
// Read first, then close
let data = account.data.borrow();
let value = u64::from_le_bytes(data[0..8].try_into()?);
drop(data);
**destination.try_borrow_mut_lamports()? += account.lamports();
**account.try_borrow_mut_lamports()? = 0;
JSON Finding
{
"detector": "account-use-after-close",
"severity": "High",
"confidence": 0.80,
"title": "Use After Close",
"description": "Account is read from after being closed, leading to stale data access.",
"cwe": [416]
}
Detection Methodology
The detector performs a single-pass scan identifying close events (full lamport transfer or zeroing at offset 0) and subsequent account uses (data reads, data writes, CPI references). It compares block and statement indices to determine ordering. The detector classifies uses as reads, writes, or CPI references for precise finding descriptions.
Limitations
- Close detection requires explicit lamport drain patterns (
AccountLamportsexpression) or zero-write at offset 0. Custom close patterns may be missed. - The detector cannot track use-after-close across separate instructions within a transaction.
- Anchor programs receive reduced confidence since the close constraint prevents this pattern.
Related Detectors
- Account Resurrection - detects resurrection after closure.
- Account Close Reopen Race - detects TOCTOU races around close operations.