Skip to main content
Sigvex

Account Reinitialization Remediation

How to prevent account reinitialization attacks by adding an initialization guard that checks the discriminator or an explicit initialized flag before overwriting account state.

Account Reinitialization Remediation

Overview

Related Detector: Account Reinitialization

Account reinitialization occurs when an initialization function can be called multiple times on an already-initialized account. An attacker can reset the account’s authority field to themselves — hijacking an account that belonged to the original owner. The fix is to check whether the account has already been initialized before writing any data, and reject the call if it has.

Before (Vulnerable)

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

pub fn initialize_vault(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
    let vault = &accounts[0];
    let mut data = vault.data.borrow_mut();

    // VULNERABLE: no check for existing initialization
    // Attacker calls this again with their own authority, overwriting the original
    data[0..8].copy_from_slice(b"vaultacc");  // Write discriminator
    data[8..40].copy_from_slice(authority.as_ref());  // Overwrite authority!
    data[40..48].copy_from_slice(&0u64.to_le_bytes()); // Reset balance to 0

    Ok(())
}

After (Fixed)

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    program_error::ProgramError,
    pubkey::Pubkey,
};

const VAULT_DISCRIMINATOR: [u8; 8] = *b"vaultacc";
const UNINITIALIZED_DISCRIMINATOR: [u8; 8] = [0u8; 8];

pub fn initialize_vault(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
    let vault = &accounts[0];
    let mut data = vault.data.borrow_mut();

    // FIXED: check whether the discriminator is already set
    // A zero discriminator means the account is freshly allocated (uninitialized)
    if data.len() < 8 {
        return Err(ProgramError::InvalidAccountData);
    }
    if data[0..8] != UNINITIALIZED_DISCRIMINATOR {
        // Discriminator already written — account has been initialized
        return Err(ProgramError::AccountAlreadyInitialized);
    }

    // Safe to initialize — account is new
    data[0..8].copy_from_slice(&VAULT_DISCRIMINATOR);
    data[8..40].copy_from_slice(authority.as_ref());
    data[40..48].copy_from_slice(&0u64.to_le_bytes());

    Ok(())
}

Alternative Mitigations

the account-validation framework’s init constraint creates the account via CPI to the System Program and sets the discriminator atomically — it cannot be called twice:

use anchor_lang::prelude::*;

#[account]
pub struct Vault {
    pub authority: Pubkey,
    pub balance: u64,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // `init` constraint:
    // 1. Creates the account via System Program CPI
    // 2. Writes the account-validation framework discriminator immediately after creation
    // 3. Fails if the account already exists (has been allocated)
    // Cannot be called twice on the same account
    #[account(
        init,
        payer = authority,
        space = 8 + 32 + 8,
    )]
    pub vault: Account<'info, Vault>,

    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let vault = &mut ctx.accounts.vault;
    vault.authority = ctx.accounts.authority.key();
    vault.balance = 0;
    Ok(())
}

2. Explicit initialized Flag

For native programs, use a dedicated boolean field:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Vault {
    pub initialized: bool,   // Guard field — always check this first
    pub authority: Pubkey,
    pub balance: u64,
}

pub fn initialize_vault(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
    let vault_account = &accounts[0];
    let mut data = vault_account.data.borrow_mut();

    // Deserialize to check initialized flag
    let mut vault: Vault = Vault::try_from_slice(&data)
        .map_err(|_| ProgramError::InvalidAccountData)?;

    if vault.initialized {
        return Err(ProgramError::AccountAlreadyInitialized);
    }

    vault.initialized = true;
    vault.authority = authority;
    vault.balance = 0;

    // Re-serialize
    vault.serialize(&mut &mut data[..])?;
    Ok(())
}

3. CFG Predecessor Validation (Advanced Native)

For multi-step initialization where the guard lives in a predecessor function, ensure the guard check is in the same execution path as the initialization write:

// Pattern: guard at the top of the function, write at the bottom
pub fn initialize(accounts: &[AccountInfo]) -> ProgramResult {
    let account = &accounts[0];
    let data_ref = account.data.borrow();

    // Guard: check FIRST, before any modification
    let is_uninitialized = data_ref[0] == 0; // First byte of discriminator is 0
    drop(data_ref); // Release borrow before mutable borrow

    if !is_uninitialized {
        return Err(ProgramError::AccountAlreadyInitialized);
    }

    // Now safe to write
    let mut data = account.data.borrow_mut();
    data[0] = 1; // Set initialized marker
    // ... write remaining fields
    Ok(())
}

Common Mistakes

Mistake 1: Checking After Writing

// WRONG: checks initialized flag AFTER overwriting it
pub fn bad_initialize(accounts: &[AccountInfo]) -> ProgramResult {
    let mut data = accounts[0].data.borrow_mut();
    data[0..8].copy_from_slice(&DISCRIMINATOR); // Already overwrote!
    if data[0..8] == ALREADY_INITIALIZED_DISCRIMINATOR { // Always false now
        return Err(ProgramError::AccountAlreadyInitialized);
    }
    Ok(())
}

Mistake 2: Using #[account(init_if_needed)] Without Thought

// DANGEROUS: init_if_needed initializes if not present but ALSO
// calls your handler when the account already exists
// Any state reset in the handler body is a reinitialization vulnerability
#[account(init_if_needed, payer = user, space = 8 + 64)]
pub vault: Account<'info, Vault>,

// Handler must not reset fields when init_if_needed re-enters on existing accounts
pub fn handler(ctx: Context<Ctx>) -> Result<()> {
    // WRONG: resets authority even for existing accounts
    ctx.accounts.vault.authority = ctx.accounts.user.key();
    Ok(())
}

If using init_if_needed, gate all field resets behind a freshly-initialized check.

Mistake 3: System Account vs. Program-Owned Check

// INCOMPLETE: checks lamports but not ownership — an account can have lamports
// and still be uninitialized if allocated but not written
if accounts[0].lamports() > 0 {
    return Err(ProgramError::AccountAlreadyInitialized);
}
// Better: check the discriminator directly

References