-
Notifications
You must be signed in to change notification settings - Fork 0
ARGUS A08
Rule Code:
ARGUS-A08Identifier:TX_EXTERNAL_IOSeverity:HIGH / CRITICAL(Connection Pool Starvation & Lock Cascading Blocker) Category:Concurrency, Performance & Transactional IntegrityTarget Standards: CWE-400 (Uncontrolled Resource Consumption), CWE-662 (Improper Synchronization), OWASP ASVS v4.0.3/v5.0 §V11.1.4
Database transaction scopes-enclosed via pgx.Tx, pool.Begin, pool.BeginTx, pgx.BeginFunc, or application transaction helpers (argus.ExecuteTx)-must never enclose blocking external non-database I/O operations.
Forbidden operations within transaction blocks include:
- Outbound HTTP / gRPC client requests
- Third-party webhook dispatches
- Disk filesystem operations (
os.ReadFile,os.WriteFile) - Subprocess execution (
exec.Command) - Arbitrary thread pauses (
time.Sleep) - Blocking channel communications (
ch <- val,<-ch) - Mutex lock acquisitions (
sync.Mutex.Lock)
Transactions must strictly execute rapid, atomic SQL operations that commit within milliseconds. Side-effects and external communications must be decoupled using the Transactional Outbox Pattern or asynchronous background job queues.
A database connection checked out from a pool (pgxpool.Pool) remains dedicated to the active transaction until COMMIT or ROLLBACK. If an application executes an external HTTP call averaging 500ms-2000ms inside the transaction, connection turnover collapses. Under moderate traffic, all pooled connections become exhausted, causing immediate application-wide cascading timeouts (HTTP 503).
-
Row & Table Locks: Any row updated or locked (
SELECT FOR UPDATE) within the transaction remains locked against concurrent modifications until commit. -
xminHorizon Stalls: PostgreSQL's autovacuum engine cannot prune dead tuples created after thexminhorizon of the oldest active transaction. Holding long-running transactions stalls cluster vacuuming, leading to table bloat and performance degradation.
flowchart TD
subgraph BAD ["External I/O in Transaction (VULNERABLE)"]
direction TB
App1["Begin Database Transaction"] --> LockRow["Acquires Row Lock (UPDATE balances)"]
LockRow --> ExtIO["Calls External Payment Gateway (HTTP 1.5s)"]
ExtIO --> PoolStarve["Connection Pool Exhausted (Starvation)"]
ExtIO --> LockConvoy["Concurrent Requests Queue Behind Row Lock"]
ExtIO --> VacBlocked["Autovacuum Blocked (xmin Horizon Stall)"]
PoolStarve --> Outage["Cluster Outage & Cascading Timeouts (CWE-400)"]
end
subgraph GOOD ["Transactional Outbox Pattern (COMPLIANT)"]
direction TB
App2["Begin Database Transaction"] --> UpdateRow["Updates Balances (< 2ms)"]
UpdateRow --> InsertOutbox["INSERT INTO outbox_events (< 1ms)"]
InsertOutbox --> CommitTx["Commit Transaction (< 1ms)"]
CommitTx --> Worker["Async Background Worker Reads Outbox & Sends HTTP"]
CommitTx --> Safe["Connection Released in < 5ms (SAFE)"]
end
Argus conducts AST inspection combined with interprocedural call graph traversal:
flowchart LR
Scan["Scan Go Functions<br/>(Exclude _test.go)"] --> TxDetect{"Contains Tx Scope?<br/>(BeginFunc, WithTx,<br/>pool.Begin..Commit)"}
TxDetect -->|No| Safe["Pass (No Transaction)"]
TxDetect -->|Yes| InspectBody["Traverse AST Nodes<br/>Inside Tx Body"]
InspectBody --> IORegistry{"Matches Blocking I/O?<br/>(time.Sleep, http, os,<br/>exec, channel, mutex)"}
IORegistry -->|Yes| Report["Report CRITICAL Violation:<br/>Blocking I/O in Tx"]
IORegistry -->|No| CallGraph{"Calls Local Helper Function?"}
CallGraph -->|Yes| HelperWalk["Walk Helper Function Body"]
HelperWalk --> IORegistry
CallGraph -->|No| Safe
-
Transaction Boundary Detection (
tx_detector.go): Identifies both closure-based transactions (pgx.BeginFunc,ExecuteTx) and explicit block scopes (pool.Beginthroughtx.Commit/tx.Rollback). -
Blocking I/O Registry (
io_registry.go): Detects standard library blocking operations (time.Sleep,http.Get/Post/Client.Do,net.Dial,os.ReadFile/WriteFile,exec.Command, channel send/receive, and mutex locking). -
Interprocedural Call Graph (
callgraph_walker.go): Follows local function calls made within the transaction block to detect hidden I/O invocations inside helpers.
| Failure Mode | Technical Impact | Risk Severity |
|---|---|---|
| HTTP Request Inside Tx | Ties up pool connection during external latency; causes cluster-wide starvation. | CRITICAL |
time.Sleep Inside Tx |
Intentionally locks connection and held rows for arbitrary duration. | CRITICAL |
| Disk I/O Inside Tx | Disk write latency and filesystem lock stalls block transaction throughput. | HIGH |
| Helper Function I/O Bypass | Obfuscated I/O behind internal helper functions escapes basic surface linters. | HIGH |
| Channel / Mutex Synchronization | Goroutine deadlocks freeze transactions indefinitely, causing connection leaks. | HIGH |
// VIOLATION: Calling external API inside transaction closure
func ProcessPayment(ctx context.Context, pool *pgxpool.Pool, orderID string) error {
return pool.BeginFunc(ctx, func(tx pgx.Tx) error {
_ = tx.Exec(ctx, "UPDATE orders SET status = 'PROCESSING' WHERE id = $1", orderID)
// Flagged: blocking external I/O (http.Post) detected inside database transaction
resp, err := http.Post("https://api.payment.com/charge", "application/json", nil)
if err != nil {
return err
}
return nil
})
}func sendNotification(userID string) {
// Outbound HTTP call
_, _ = http.Get("https://notify.service/send?user=" + userID)
}
// VIOLATION: Calling helper that performs I/O inside explicit transaction
func ActivateAccount(ctx context.Context, pool *pgxpool.Pool, userID string) error {
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_ = tx.Exec(ctx, "UPDATE users SET active = true WHERE id = $1", userID)
// Flagged: blocking external I/O (http.Get) detected via helper "sendNotification"
sendNotification(userID)
return tx.Commit(ctx)
}// COMPLIANT: Decoupled notification via outbox table
func ActivateAccount(ctx context.Context, pool *pgxpool.Pool, userID string) error {
return pool.BeginFunc(ctx, func(tx pgx.Tx) error {
// 1. Mutate application state
if _, err := tx.Exec(ctx, "UPDATE users SET active = true WHERE id = $1", userID); err != nil {
return err
}
// 2. Record outbox event in the same atomic transaction (< 2ms)
const outboxSQL = "INSERT INTO outbox_events (event_type, payload) VALUES ($1, $2)"
_, err := tx.Exec(ctx, outboxSQL, "user.activated", userID)
return err
})
}// COMPLIANT: Perform external I/O outside transaction boundaries
func ProcessOrder(ctx context.Context, pool *pgxpool.Pool, req PaymentRequest) error {
// 1. External I/O executed before transaction
chargeResult, err := paymentGateway.Charge(ctx, req)
if err != nil {
return err
}
// 2. Fast database update inside short transaction
return pool.BeginFunc(ctx, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, "UPDATE orders SET paid = true, ref = $1 WHERE id = $2", chargeResult.ID, req.OrderID)
return err
})
}For test harnesses, benchmark harnesses, or intentional latency injection:
// argus:ignore ARGUS-A08 load testing artificial latency injection
time.Sleep(200 * time.Millisecond)Alternatively, use the identifier alias:
// argus:ignore TX_EXTERNAL_IO verified test harness mock dispatch
resp, err := http.Post(testServerURL, "application/json", body)Enable or configure this rule in .argus.yaml:
rules:
ARGUS-A08:
enabled: true