Skip to main content
Sigvex

Shared Mutable Race

Detects race conditions from shared mutable account access where the same account is modified through multiple paths without duplicate checks.

Shared Mutable Race

Overview

Remediation Guide: How to Fix Shared Mutable Race Conditions

The shared mutable race detector identifies Solana programs where the same mutable account is written to through multiple execution paths or through both direct writes and CPI calls without proper duplicate account validation. In Solana, accounts are passed as a flat array, and the program is responsible for ensuring that the same account is not passed in multiple mutable positions. Without duplicate checks, concurrent modifications can corrupt state.

Sigvex tracks all write operations (direct StoreAccountData and CPI writes) for each account variable, along with duplicate account validation patterns. Accounts that are written to multiple times without validation or that are modified both locally and via CPI are flagged. CWE mapping: CWE-362 (Concurrent Execution Using Shared Resource with Improper Synchronization).

Why This Is an Issue

Shared mutable access creates several attack vectors:

  • Double-spend via aliasing: If the same token account is passed as both source and destination in a transfer, balance calculations can produce incorrect results.
  • State corruption: Multiple writes to the same account offset from different code paths can overwrite each other, leaving the account in an inconsistent state.
  • CPI write conflicts: A local write followed by a CPI that also writes to the same account can produce unpredictable results, since the CPI may read stale pre-write data.
  • Reentrancy-like effects: While Solana prevents true reentrancy, shared mutable accounts in CPI chains can produce similar state confusion.

How to Resolve

Native Rust

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn safe_transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let source = &accounts[0];
    let destination = &accounts[1];

    // Check for duplicate accounts
    if source.key == destination.key {
        return Err(ProgramError::InvalidArgument);
    }

    **source.lamports.borrow_mut() -= amount;
    **destination.lamports.borrow_mut() += amount;
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct SafeTransfer<'info> {
    #[account(mut, constraint = source.key() != destination.key() @ ErrorCode::DuplicateAccount)]
    pub source: Account<'info, TokenAccount>,
    #[account(mut)]
    pub destination: Account<'info, TokenAccount>,
}

Examples

Vulnerable Code

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn process_rewards(accounts: &[AccountInfo]) -> ProgramResult {
    let reward_pool = &accounts[0];
    let user_account = &accounts[1];

    // Write 1: Update reward pool
    let mut pool_data = reward_pool.data.borrow_mut();
    pool_data[0..8].copy_from_slice(&new_pool_balance.to_le_bytes());
    drop(pool_data);

    // CPI that also modifies reward_pool -- potential conflict
    solana_program::program::invoke(
        &update_rewards_instruction(reward_pool.key),
        &[reward_pool.clone(), user_account.clone()],
    )?;

    // No duplicate check -- if user_account == reward_pool, state is corrupted
    Ok(())
}

Fixed Code

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn process_rewards(accounts: &[AccountInfo]) -> ProgramResult {
    let reward_pool = &accounts[0];
    let user_account = &accounts[1];

    // Duplicate account check
    if reward_pool.key == user_account.key {
        return Err(ProgramError::InvalidArgument);
    }

    let mut pool_data = reward_pool.data.borrow_mut();
    pool_data[0..8].copy_from_slice(&new_pool_balance.to_le_bytes());
    drop(pool_data);

    solana_program::program::invoke(
        &update_rewards_instruction(reward_pool.key),
        &[reward_pool.clone(), user_account.clone()],
    )?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "shared-mutable-race",
  "severity": "high",
  "confidence": 0.78,
  "description": "Account v0 has multiple write operations (direct write at block 0 stmt 1, CPI write at block 0 stmt 3) without duplicate account validation. Concurrent modifications through different paths can corrupt account state.",
  "location": { "function": "process_rewards", "offset": 1 }
}

Detection Methodology

The detector performs a two-pass analysis:

  1. Operation collection: Tracks all write operations per account variable, categorized as direct writes (StoreAccountData) or CPI writes (InvokeCpi). Also collects duplicate account validation patterns (comparison operations).
  2. Race detection: Reports accounts with multiple writes lacking validation, and accounts modified both locally and via CPI without duplicate checks.

Context modifiers:

  • Anchor programs: confidence reduced by 50% (Anchor’s type system provides some protection)
  • Admin/initialization functions: confidence reduced by 40%
  • Read-only/view functions: not flagged (no mutable operations)

Limitations

False positives:

  • Sequential writes to the same account that are intentional (e.g., initializing multiple fields) may be flagged as multiple writes.
  • Programs that validate account distinctness through indirect means (e.g., PDA derivation with different seeds guarantees distinct accounts) may be flagged.

False negatives:

  • Race conditions between separate transactions modifying the same account are not detectable at the program level.
  • Duplicate accounts passed via remaining_accounts that are not tracked as individual variables may be missed.

References