Skip to main content
Sigvex

Implicit Instruction Ordering Remediation

How to fix implicit instruction ordering dependencies not enforced on all control flow paths.

Implicit Instruction Ordering Remediation

Overview

Related Detector: Implicit Instruction Ordering

Implicit instruction ordering vulnerabilities occur when required validation (signer checks, owner checks, writable checks) exists on some control flow paths but not all. An attacker can reach a sensitive operation via the unvalidated path. The fix is to move all validation before any branching, so every downstream path inherits the validated state.

Before (Vulnerable)

pub fn process(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let user = &accounts[0];
    let vault = &accounts[1];

    if data[0] == 0 {
        // Path A: has signer check
        if !user.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }
        **vault.try_borrow_mut_lamports()? -= 100;
    } else {
        // Path B: missing signer check
        **vault.try_borrow_mut_lamports()? -= 50;
    }
    Ok(())
}

After (Fixed)

pub fn process(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let user = &accounts[0];
    let vault = &accounts[1];

    // All validation before branching
    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    if !vault.is_writable {
        return Err(ProgramError::InvalidArgument);
    }

    if data[0] == 0 {
        **vault.try_borrow_mut_lamports()? -= 100;
    } else {
        **vault.try_borrow_mut_lamports()? -= 50;
    }
    Ok(())
}

Alternative Mitigations

Use an account-validation framework constraints to enforce validation before the handler executes:

#[derive(Accounts)]
pub struct Withdraw<'info> {
    pub user: Signer<'info>,           // Enforces signer check
    #[account(mut, has_one = user)]
    pub vault: Account<'info, Vault>,  // Enforces owner + writable
}

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // All paths are validated before this point
    ctx.accounts.vault.balance -= amount;
    Ok(())
}

Extract a validated context struct that is constructed only after all checks pass. The handler receives the validated struct and cannot bypass checks:

struct ValidatedContext<'a> {
    authority: &'a AccountInfo<'a>,
    vault: &'a AccountInfo<'a>,
}

fn validate(accounts: &[AccountInfo]) -> Result<ValidatedContext, ProgramError> {
    let authority = &accounts[0];
    let vault = &accounts[1];
    if !authority.is_signer { return Err(MissingRequiredSignature); }
    if vault.owner != &crate::ID { return Err(IncorrectProgramId); }
    Ok(ValidatedContext { authority, vault })
}

Common Mistakes

Duplicating validation in each branch. While technically correct, this is error-prone and difficult to maintain. Move shared validation before the branch instead.

Validating the signer but not the owner (or vice versa). Each sensitive operation type has specific validation requirements. Transfers need signer checks, stores need writable checks, and data reads need owner checks.

Assuming CPI targets validate on behalf of the caller. Each program must validate its own preconditions. A CPI callee validates its own constraints, not the constraints of the calling program.

References