-
Notifications
You must be signed in to change notification settings - Fork 0
ARGUS A10
Rule Code:
ARGUS-A10Identifier:ISOLATION_LEVELSeverity:HIGH / CRITICAL(Financial Write-Skew & Silent Data Corruption Blocker) Category:Data Integrity, Concurrency & Financial CorrectnessTarget Standards: CWE-362 (Race Condition / Concurrent Execution), CWE-662 (Improper Synchronization), OWASP ASVS v4.0.3/v5.0 §V11.1.4, ACID Isolation Standards
Database transactions mutating critical tables-such as financial accounts, balances, inventory quotas, or sequential numbering (balances, accounts, inventory, wallets, ledger, sequence_counters)-must not implicitly rely on default ReadCommitted isolation.
Every transaction writing to critical tables must enforce at least one of 3 concurrency safeguards:
- Specifying
pgx.RepeatableReadorpgx.Serializableexplicitly viapgx.TxOptions, OR - Acquiring explicit pessimistic row locks (
SELECT ... FOR UPDATE) on the target rows before mutation underReadCommitted, OR - Enclosing the transaction within a transaction-level advisory lock (
argus.WithAdvisoryLock).
Under PostgreSQL's default ReadCommitted isolation level, each SQL statement within a transaction executes against a fresh MVCC snapshot:
- Concurrent Overlapping Reads: Two concurrent transactions (Tx A and Tx B) read the current ledger balance of $10,000 simultaneously.
-
False Precondition Validation: Tx A validates a $8,000 withdrawal (
10,000 >= 8,000-> valid). Tx B validates a $7,000 withdrawal (10,000 >= 7,000-> valid). -
Sequential Commit Catastrophe: Both transactions commit sequential
UPDATEstatements. The resulting balance drops to -$5,000, creating silent financial corruption (CWE-362).
flowchart TD
subgraph SKEW ["Write-Skew Catastrophe (ReadCommitted)"]
direction TB
A1["Tx A: Reads Balance ($10,000)"] --> V1["Validates $8k Payout (PASS)"]
B1["Tx B: Reads Balance ($10,000)"] --> V2["Validates $7k Payout (PASS)"]
V1 --> U1["Tx A Updates Balance to $2,000 & COMMITS"]
V2 --> U2["Tx B Updates Balance to -$5,000 & COMMITS"]
U2 --> Outage["Silent Financial Corruption & Ledger Deficit (CWE-362)"]
end
subgraph SAFE ["Serializable Snapshot Isolation (COMPLIANT)"]
direction TB
A2["Tx A (Serializable): Reads Balance ($10,000)"] --> S1["Validates $8k & Updates to $2k"]
B2["Tx B (Serializable): Reads Balance ($10,000)"] --> S2["Validates $7k & Attempts Update"]
S1 --> C1["Tx A Commits Successfully"]
S2 --> Err["Tx B Aborted by PostgreSQL with SQLSTATE 40001 (SAFE)"]
end
PostgreSQL implements true mathematical SSI (src/backend/storage/lmgr/predicate.c, Cahill et al.). SSI tracks read-write anti-dependencies (SIReadLock) in shared memory without blocking reads. When an anomaly cycle is detected, PostgreSQL automatically aborts one transaction with SQLSTATE 40001 (serialization failure), guaranteeing 100% data integrity without manual lock coordination.
Argus inspects transaction scopes across database calls and helper closures:
flowchart LR
Scan["Inspect Go Functions<br/>(Exclude _test.go)"] --> DetectTx{"Identifies Transaction Scope?<br/>(Begin, BeginTx, WithTx)"}
DetectTx -->|Yes| CheckSQL["Inspect Queries in Tx Body"]
CheckSQL --> CriticalCheck{"Writes to Critical Table?<br/>(critical_tables.go)"}
CriticalCheck -->|Yes| GuardCheck{"Has Concurrency Guard?<br/>1. Serializable/RepeatableRead<br/>2. SELECT ... FOR UPDATE<br/>3. argus.WithAdvisoryLock"}
GuardCheck -->|No| Report["Report CRITICAL Violation:<br/>Unisolated Critical Table Write"]
GuardCheck -->|Yes| Pass["Pass (Safeguarded Mutation)"]
CriticalCheck -->|No| Pass
DetectTx -->|No| Pass
-
Critical Table Registry (
critical_tables.go): Analyzes SQL AST forINSERT,UPDATE, andDELETEstatements targeting registered critical tables (balances,accounts,inventory,wallets,ledger, etc.). -
Pessimistic Row Lock Detector (
pessimistic_lock.go): Parses PostgreSQL ASTLockingClauseto verifySELECT ... FOR UPDATEorFOR NO KEY UPDATE. -
Transaction Options Validator (
tx_options_check.go): Analyzespgx.TxOptionsstructs to verifyIsoLevel: SerializableorRepeatableRead.
| Failure Mode | Technical Impact | Risk Severity |
|---|---|---|
Default ReadCommitted on Balances |
Triggers write-skew anomalies leading to negative balances and ledger corruption. | CRITICAL |
| Quota Allocation Race Conditions | Over-allocates constrained inventory or subsidy quotas beyond authorized limits. | HIGH |
| Sequential Numbering Collision | Concurrent counter reads generate duplicate invoice or certificate serial numbers. | HIGH |
// VIOLATION: Mutating balance under default ReadCommitted without row locking
func DeductAccountBalance(ctx context.Context, pool *pgxpool.Pool, accountID int, amount int64) error {
// Flagged: transaction writing to critical table without explicit isolation or row lock
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, "UPDATE saldo SET amount = amount - $1 WHERE account_id = $2", amount, accountID)
if err != nil {
return err
}
return tx.Commit(ctx)
}// VIOLATION: WithTx on critical table without TxOptions
func AllocateQuota(ctx context.Context, pool *pgxpool.Pool, quotaID int) error {
// Flagged: transaction writing to critical table without explicit isolation or row lock
return argus.WithTx(ctx, pool, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, "UPDATE kuota SET sisa = sisa - 1 WHERE id = $1", quotaID)
return err
})
}// COMPLIANT: Serializable isolation prevents write-skew mathematically
func DeductAccountBalance(ctx context.Context, pool *pgxpool.Pool, accountID int, amount int64) error {
opts := pgx.TxOptions{IsoLevel: pgx.Serializable}
tx, err := pool.BeginTx(ctx, opts)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, "UPDATE saldo SET amount = amount - $1 WHERE account_id = $2", amount, accountID)
if err != nil {
return err
}
return tx.Commit(ctx)
}// COMPLIANT: Explicit row lock under ReadCommitted
func TransferFunds(ctx context.Context, pool *pgxpool.Pool, fromID, toID int, amount int64) error {
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// Lock rows exclusively before mutation
const lockSQL = "SELECT amount FROM saldo WHERE id = $1 FOR UPDATE"
var balance int64
if err := tx.QueryRow(ctx, lockSQL, fromID).Scan(&balance); err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE saldo SET amount = amount - $1 WHERE id = $2", amount, fromID)
if err != nil {
return err
}
return tx.Commit(ctx)
}// COMPLIANT: Protected by distributed advisory lock
func AllocateQuota(ctx context.Context, pool *pgxpool.Pool, quotaID int) error {
lockKey := fmt.Sprintf("quota:allocation:%d", quotaID)
return argus.WithAdvisoryLock(ctx, lockKey, func() error {
return argus.WithTx(ctx, pool, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, "UPDATE kuota SET sisa = sisa - 1 WHERE id = $1", quotaID)
return err
})
})
}For manual database migrations, admin reconciliation scripts, or batch repair tools:
// argus:ignore ARGUS-A10 manual single-operator data migration
tx, err := pool.Begin(ctx)Alternatively, use the identifier alias:
// argus:ignore ISOLATION_LEVEL single-tenant reconciliation batch
err := argus.WithTx(ctx, pool, migrationFn)Configure additional custom critical tables in .argus.yaml:
rules:
ARGUS-A10:
enabled: true
critical_tables:
- ledger_entries
- voucher_pool
- order_sequence