Account Reinitialization
Detects account data writes that lack a prior initialization check, allowing attackers to reset account state or overwrite existing data.
Account Reinitialization
Overview
Remediation Guide: How to Fix Account Reinitialization
The account reinitialization detector identifies Solana program functions that write to account data without first checking whether the account is already initialized. When an initialization function lacks this guard, an attacker can call it multiple times: once to legitimately initialize the account and once more (or many more times) to overwrite the account’s state, potentially resetting counters, stealing authority, or corrupting financial data.
Sigvex uses CFG-based predecessor analysis (BFS over the control-flow graph) to determine whether a read of the account’s initialization status (typically offset 0 discriminator bytes) occurs on any path leading to a StoreAccountData write. Findings are generated when a write occurs with no detectable prior initialization check.
Why This Is an Issue
Account reinitialization is particularly dangerous when:
- The account holds authority or admin keys: a reinitializer can set themselves as the new authority
- The account tracks balances: a reinitializer can reset balances to zero or to arbitrary values
- The account stores a nonce: resetting the nonce breaks replay protection
The attack is straightforward: the attacker calls the initialization function on an already-initialized account. If the program lacks an “is initialized?” check, it happily overwrites the existing state.
Anchor’s #[account(init)] prevents this automatically by requiring the account to be uninitialized (owned by the system program). However, init_if_needed has different semantics and can still be vulnerable, and native programs must implement this check manually.
CWE mapping: CWE-913 (Improper Control of Dynamically-Managed Code Resources).
How to Resolve
// Before: Vulnerable — no initialization check
pub fn initialize(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
let state_account = &accounts[0];
let mut data = state_account.data.borrow_mut();
// VULNERABLE: if called twice, overwrites the existing authority
data[0..8].copy_from_slice(b"stateacc"); // Discriminator
data[8..40].copy_from_slice(authority.as_ref());
Ok(())
}
// After: Check initialization before writing
const STATE_DISCRIMINATOR: &[u8] = b"stateacc";
pub fn initialize(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
let state_account = &accounts[0];
let data = state_account.data.borrow();
// FIXED: fail if already initialized
if data[0..8] == *STATE_DISCRIMINATOR {
return Err(ProgramError::AccountAlreadyInitialized);
}
drop(data);
let mut data = state_account.data.borrow_mut();
data[0..8].copy_from_slice(STATE_DISCRIMINATOR);
data[8..40].copy_from_slice(authority.as_ref());
Ok(())
}
For Anchor:
// Anchor #[account(init)] prevents reinitialization automatically
#[derive(Accounts)]
pub struct Initialize<'info> {
// init: account must be uninitialized (system-owned, zero data)
// Anchor transfers ownership to this program and sets discriminator
#[account(
init,
payer = payer,
space = 8 + 32 + 8, // discriminator + authority pubkey + u64
)]
pub state: Account<'info, State>,
#[account(mut, signer)]
pub payer: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
Examples
Vulnerable Code
pub fn initialize_user_state(
accounts: &[AccountInfo],
authority: &Pubkey,
initial_balance: u64,
) -> ProgramResult {
let user_state = &accounts[0];
let mut data = user_state.data.borrow_mut();
// VULNERABLE: no check — attacker calls this twice to reset state
// First call: sets legitimate authority and initial_balance
// Second call: sets attacker as authority, resets initial_balance = u64::MAX
data[0..32].copy_from_slice(authority.as_ref());
data[32..40].copy_from_slice(&initial_balance.to_le_bytes());
Ok(())
}
Fixed Code
const INITIALIZED_FLAG: u8 = 1;
pub fn initialize_user_state(
accounts: &[AccountInfo],
authority: &Pubkey,
initial_balance: u64,
) -> ProgramResult {
let user_state = &accounts[0];
{
let data = user_state.data.borrow();
// FIXED: check initialization flag at byte 0
if data[0] == INITIALIZED_FLAG {
return Err(ProgramError::AccountAlreadyInitialized);
}
}
let mut data = user_state.data.borrow_mut();
data[0] = INITIALIZED_FLAG;
data[1..33].copy_from_slice(authority.as_ref());
data[33..41].copy_from_slice(&initial_balance.to_le_bytes());
Ok(())
}
Sample Sigvex Output
{
"detector_id": "account-reinitialization",
"severity": "medium",
"confidence": 0.75,
"description": "Account v0 appears to be initialized without first checking if it's already initialized. This could allow an attacker to reset account state or corrupt data. Writing discriminator (offset 0) without validation is critical.",
"location": { "function": "initialize_user_state", "offset": 3 }
}
Detection Methodology
The detector uses CFG predecessor analysis:
- Early exit: Functions without
StoreAccountDatastatements are skipped. - Predecessor map construction: BFS constructs a map of CFG block predecessors.
- Validation tracking: Reads of offset 0 with
Size::ByteorSize::Word(the discriminator region) mark the account as having an initialization check. - Branch-based checks:
HirStmt::Branchconditions containing discriminator reads are also tracked. - Write validation: For each
StoreAccountData, the detector checks whether an initialization read exists in the current block or any predecessor block (BFS traversal). - Severity/confidence: Discriminator writes (offset 0) at confidence 0.75; other offset writes at confidence 0.60.
- Context modifiers: Admin functions reduce by
ADMIN_FUNCTION_CONFIDENCE_MULTIPLIER. Anchor programs with discriminator validation reduce by 0.25x. PDA-derived accounts reduce by 0.50x. Read-only functions reduce by 0.30x.
Limitations
False positives:
- Functions that intentionally reinitialize accounts (e.g., a reset/close-and-reinit pattern) may be incorrectly flagged.
- Anchor
#[account(init)]handles this automatically — Anchor programs receive reduced confidence. - Programs where the initialization check is in a predecessor instruction (separate transaction) are not detectable.
False negatives:
- Initialization checks that use a custom field rather than offset 0 may not be recognized.
- Programs that check initialization via an owner check (uninitialized accounts are system-owned) are not detected by this specific pattern.
Related Detectors
- Missing Owner Check — detects missing account ownership validation that can enable reinitialization
- Type Cosplay — detects discriminator spoofing related to unvalidated account types