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.
Recommended Fix
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
- Anchor’s discriminator: Use Anchor’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
}
-
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.
-
Zero-copy deserialization with bounds checking: Use
bytemuckor 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.