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.
Recommended Fix
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
1. Anchor init Constraint (Recommended)
Anchor’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 Anchor 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