Skip to main content
Sigvex

Account Layout Version Remediation

How to fix account deserialization that lacks version validation.

Remediating Account Layout Version Issues

Overview

Related Detector: Account Layout Version

When account data is read without validating the layout version, field offset mismatches from account structure upgrades cause silent data corruption. The fix is to include a version field in every account structure and validate it before accessing any data fields.

Before (Vulnerable)

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

pub fn read_vault(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = &accounts[0];
    let data = vault.data.borrow();

    // Reads fields without version check
    let balance = u64::from_le_bytes(data[32..40].try_into()?);
    let authority = &data[40..72];
    Ok(())
}

After (Fixed)

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

const CURRENT_VERSION: u8 = 2;

pub fn read_vault(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = &accounts[0];
    let data = vault.data.borrow();

    // Validate version first
    let version = data[8]; // After 8-byte discriminator
    match version {
        1 => read_vault_v1(&data),
        2 => read_vault_v2(&data),
        _ => Err(ProgramError::InvalidAccountData),
    }
}

Alternative Mitigations

  1. the account-validation framework’s discriminator: Use the account-validation framework’s built-in 8-byte discriminator for type safety. Combine with a user-defined version field for layout migrations.
#[account]
pub struct VaultV2 {
    pub version: u8,       // User-defined version
    pub balance: u64,
    pub authority: Pubkey,
    pub new_field: u64,    // Added in V2
}
  1. Migration instruction: Create a dedicated migration instruction that reads V1 accounts, converts them to V2 layout, and writes back. After migration completes, remove V1 support.

  2. Zero-copy deserialization with bounds checking: Use bytemuck or manual offset reads with explicit length validation to prevent out-of-bounds reads on smaller V1 accounts.

let data = account.data.borrow();
if data.len() < EXPECTED_V2_SIZE {
    return Err(ProgramError::InvalidAccountData);
}

Common Mistakes

  • Using version 0 as default: Uninitialized accounts have zeroed data, so version 0 looks like a valid version. Start version numbering at 1.
  • Checking version but not switching on it: Reading the version field but always using the V2 layout regardless of the value defeats the purpose.
  • Forgetting to update the version on write: When an instruction creates or modifies an account, it must write the current version number.
  • Not validating data length alongside version: A V2 account should have at least V2_SIZE bytes. An account with version 2 but V1 length indicates corruption.
  • Removing V1 support too early: During migration, both versions coexist. Remove V1 support only after all accounts have been migrated.

References