Account Layout Version
Detects account deserialization without version validation, which can cause silent data corruption when account structures are upgraded.
Account Layout Version
Overview
Remediation Guide: How to Fix Account Layout Version Issues
The account layout version detector identifies Solana programs that read account data fields beyond the metadata region (discriminator and version) without first validating the account’s layout version. When programs upgrade account structures — adding, removing, or reordering fields — reading old accounts with new code (or vice versa) causes field offset mismatches, leading to silent data corruption or security vulnerabilities.
Sigvex identifies deserialization sites (account data reads at offsets beyond 16 bytes), version field reads (offsets 0 or 8), and version checks (conditional branches using version variables). Accounts read beyond the metadata region without a corresponding version check are flagged. CWE mapping: CWE-704 (Incorrect Type Conversion or Cast).
Why This Is an Issue
Account layout upgrades are common in Solana programs that evolve over time. Without version validation:
- Field offset mismatches: A V1 account read as V2 interprets bytes at wrong offsets, producing incorrect values for balances, authorities, or timestamps.
- Silent corruption: Unlike deserialization failures that produce errors, offset mismatches silently return wrong data that may pass other validation checks.
- Security escalation: A misinterpreted authority field could point to an attacker-controlled account. A misinterpreted balance could allow over-withdrawal.
- Migration hazards: During a migration period, both V1 and V2 accounts coexist. Programs that do not distinguish between versions may corrupt migrated accounts.
How to Resolve
Native Rust
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
const CURRENT_VERSION: u8 = 2;
pub fn process_account(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
let data = account.data.borrow();
// Read and validate version before accessing fields
let version = data[8]; // Version at offset 8 (after 8-byte discriminator)
match version {
1 => process_v1(&data),
2 => process_v2(&data),
_ => Err(ProgramError::InvalidAccountData),
}
}
fn process_v1(data: &[u8]) -> ProgramResult {
let balance = u64::from_le_bytes(data[9..17].try_into()?);
// V1 layout processing
Ok(())
}
fn process_v2(data: &[u8]) -> ProgramResult {
let balance = u64::from_le_bytes(data[9..17].try_into()?);
let extra_field = u64::from_le_bytes(data[17..25].try_into()?);
// V2 layout processing
Ok(())
}
Anchor
#[account]
pub struct MyAccountV2 {
pub version: u8,
pub balance: u64,
pub extra_field: u64, // Added in V2
}
pub fn process(ctx: Context<Process>) -> Result<()> {
let account = &ctx.accounts.my_account;
require!(account.version == 2, ErrorCode::UnsupportedVersion);
// Safe to access V2 fields
Ok(())
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn read_balance(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
let data = account.data.borrow();
// Reads balance at offset 32 without checking version
// If this is a V1 account, offset 32 may contain a different field!
let balance = u64::from_le_bytes(data[32..40].try_into()?);
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn read_balance(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
let data = account.data.borrow();
let version = data[8];
if version != CURRENT_VERSION {
return Err(ProgramError::InvalidAccountData);
}
let balance = u64::from_le_bytes(data[32..40].try_into()?);
Ok(())
}
Sample Sigvex Output
{
"detector_id": "account-layout-version",
"severity": "high",
"confidence": 0.78,
"description": "Account v1 data is accessed at offset 32 without validating the account layout version. When account structures are upgraded, this can cause silent data corruption.",
"location": { "function": "read_balance", "offset": 0 }
}
Detection Methodology
The detector performs a two-pass analysis:
- Deserialization site collection: Identifies all account data reads at offsets beyond 16 (the metadata region covering discriminator and version fields).
- Version check collection: Identifies version field reads (offsets 0 or 8) that are subsequently used in conditional branches. Accounts where the version field is read but never used in a conditional are treated as unvalidated.
Context modifiers:
- Anchor programs with discriminator validation: confidence reduced by 70% (8-byte discriminator provides type-safe deserialization)
- Admin/initialization functions: confidence reduced by 40%
- Read-only/view functions: confidence reduced by 60%
Limitations
False positives:
- Programs that have never upgraded their account layout and have no plans to do so may be flagged. The version check is preventive, not reactive.
- Accounts with fixed layouts (e.g., system accounts, sysvar accounts) do not need version checks and may be flagged.
False negatives:
- Version checks performed through Anchor’s discriminator mechanism (which validates the account type but not a user-defined version field) are not tracked as explicit version checks.
- Dynamic offset computation (where the offset is a variable rather than a constant) is not analyzed.
Related Detectors
- Account Type Confusion — detects deserialization of wrong account types
- Account Reinitialization — detects accounts that can be re-initialized with different layouts