Skip to main content
Sigvex

Instruction Fallback Handler Remediation

How to fix unsafe fallback logic for unrecognized instruction discriminators.

Instruction Fallback Handler Remediation

Overview

Related Detector: Instruction Fallback Handler

An unsafe fallback handler allows unrecognized instruction discriminators to execute code that was never intended to be callable. The fix requires ensuring every discriminator match includes a catch-all arm that returns an error, and that entry point functions always read and validate the discriminator before dispatching.

Before (Vulnerable)

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let tag = instruction_data[0];
    if tag == 0 {
        return handle_init(accounts, instruction_data);
    }
    if tag == 1 {
        return handle_transfer(accounts, instruction_data);
    }
    // No error for unknown tags -- falls through silently
    Ok(())
}

After (Fixed)

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    if instruction_data.is_empty() {
        return Err(ProgramError::InvalidInstructionData);
    }
    match instruction_data[0] {
        0 => handle_init(accounts, &instruction_data[1..]),
        1 => handle_transfer(accounts, &instruction_data[1..]),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

Alternative Mitigations

Use the account-validation framework’s dispatch system. Anchor generates unique 8-byte discriminators from the instruction name hash and rejects any instruction whose discriminator does not match a declared handler:

#[program]
pub mod my_program {
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> { Ok(()) }
    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> { Ok(()) }
    // Unknown discriminators automatically return an error
}

Use an enum-based dispatch pattern for native programs to ensure exhaustive matching:

pub enum Instruction {
    Initialize,
    Transfer { amount: u64 },
}

impl Instruction {
    pub fn unpack(data: &[u8]) -> Result<Self, ProgramError> {
        let (tag, rest) = data.split_first()
            .ok_or(ProgramError::InvalidInstructionData)?;
        match tag {
            0 => Ok(Self::Initialize),
            1 => { /* parse amount from rest */ Ok(Self::Transfer { amount }) },
            _ => Err(ProgramError::InvalidInstructionData),
        }
    }
}

Common Mistakes

Using if/else if chains without a final else that errors. The last branch silently succeeds for any unrecognized value.

Returning Ok(()) in the default case. A no-op default appears harmless but allows attackers to submit garbage transactions that succeed, potentially interfering with transaction ordering assumptions.

Checking the discriminator after performing side effects. If the program writes account data before validating the discriminator, the validation is ineffective for preventing unauthorized state changes.

References