Skip to main content
Sigvex

Zero Copy Deserialization

Detects unsafe zero-copy deserialization patterns that can lead to memory corruption.

Zero Copy Deserialization

Overview

Remediation Guide: How to Fix Zero Copy Deserialization

The zero-copy deserialization detector identifies unsafe patterns when programs directly interpret account data as typed structures without proper validation. Zero-copy deserialization (via bytemuck, zerocopy, or Anchor’s zero_copy) avoids copying data but introduces risks: buffer over-reads from insufficient size validation, type confusion from missing discriminator checks, and undefined behavior from alignment violations.

Sigvex tracks account data accesses of 8 bytes or more (zero-copy candidates) and checks for size validation, discriminator checks, and alignment verification before the access point.

Why This Is an Issue

  • Buffer over-read: accessing 8 bytes from a 4-byte account reads beyond the allocation, exposing adjacent memory.
  • Type confusion: without discriminator validation, the program may interpret one account type as another, misaligning fields.
  • Alignment violations: casting unaligned data to types requiring alignment (u64, i64) causes undefined behavior on some platforms.
  • Uninitialized data: padding bytes in zero-copy structs may contain data from previous occupants.

CWE mapping: CWE-126 (Buffer Over-read), CWE-843 (Type Confusion), CWE-704 (Incorrect Type Conversion).

How to Resolve

Native Solana

pub fn process(account: &AccountInfo) -> ProgramResult {
    let data = account.data.borrow();

    // 1. Size validation
    if data.len() < std::mem::size_of::<MyStruct>() + 8 {
        return Err(ProgramError::InvalidAccountData);
    }

    // 2. Discriminator validation
    if data[0..8] != MY_DISCRIMINATOR {
        return Err(ProgramError::InvalidAccountData);
    }

    // 3. Safe zero-copy access
    let state: &MyStruct = bytemuck::try_from_bytes(&data[8..8 + std::mem::size_of::<MyStruct>()])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    // ...
}

Anchor

// Anchor's zero_copy handles alignment and validation
#[account(zero_copy)]
pub struct LargeState {
    pub data: [u64; 1024],
}

#[derive(Accounts)]
pub struct Process<'info> {
    #[account(mut)]
    pub state: AccountLoader<'info, LargeState>,
}

Examples

Vulnerable

let data = account.data.borrow();
// No size check, no discriminator check, no alignment check
let state: &MyStruct = bytemuck::from_bytes(&data[8..]);

Fixed

let data = account.data.borrow();
require!(data.len() >= 8 + std::mem::size_of::<MyStruct>(), InvalidData);
require!(data[0..8] == MY_DISCRIMINATOR, InvalidType);
let state: &MyStruct = bytemuck::try_from_bytes(&data[8..8 + std::mem::size_of::<MyStruct>()])
    .map_err(|_| ProgramError::InvalidAccountData)?;

JSON Finding

{
  "detector": "zero-copy-deserialization",
  "severity": "Critical",
  "confidence": 0.80,
  "title": "Completely Unvalidated Zero-Copy Deserialization",
  "description": "Account data is directly accessed with NO validation.",
  "cwe": [129]
}

Detection Methodology

The detector collects validation state (minimum validated size, discriminator checks, alignment checks) from branch conditions, then compares this state against data access operations. Accesses of 8 bytes or more without corresponding validation generate findings at appropriate severity levels: critical for no validation, high for missing size or discriminator checks, medium for missing alignment checks.

Limitations

  • The detector uses access size as a heuristic for zero-copy candidates (8+ bytes). Small accesses may still be zero-copy but are not flagged.
  • Validation checks in helper functions or prior instructions are not tracked.
  • Anchor’s AccountLoader provides safe zero-copy access but may not be distinguishable in decompiled bytecode.

References