# ARGUS-A08: External Blocking I/O Inside Transactions
> **Rule Code:** `ARGUS-A08`
> **Identifier:** `TX_EXTERNAL_IO`
> **Severity:** `HIGH / CRITICAL` (Connection Pool Starvation & Lock Cascading Blocker)
> **Category:** `Concurrency, Performance & Transactional Integrity`
> **Target Standards:** CWE-400 (Uncontrolled Resource Consumption), CWE-662 (Improper Synchronization), OWASP ASVS v4.0.3/v5.0 §V11.1.4
---
## 1. Overview & Core Invariant
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.
---
## 2. Technical Grounding & PostgreSQL Engine Realities
### 2.1. Connection Pool Starvation & Latency Amplification
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).
### 2.2. Lock Cascades & Autovacuum Horizon Stalls
1. **Row & Table Locks:** Any row updated or locked (`SELECT FOR UPDATE`) within the transaction remains locked against concurrent modifications until commit.
2. **`xmin` Horizon Stalls:** PostgreSQL's autovacuum engine cannot prune dead tuples created after the `xmin` horizon of the oldest active transaction. Holding long-running transactions stalls cluster vacuuming, leading to table bloat and performance degradation.
```mermaid
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
```
---
## 3. How Argus Detects Violations (Static Analysis Architecture)
Argus conducts AST inspection combined with interprocedural call graph traversal:
```mermaid
flowchart LR
Scan["Scan Go Functions
(Exclude _test.go)"] --> TxDetect{"Contains Tx Scope?
(BeginFunc, WithTx,
pool.Begin..Commit)"}
TxDetect -->|No| Safe["Pass (No Transaction)"]
TxDetect -->|Yes| InspectBody["Traverse AST Nodes
Inside Tx Body"]
InspectBody --> IORegistry{"Matches Blocking I/O?
(time.Sleep, http, os,
exec, channel, mutex)"}
IORegistry -->|Yes| Report["Report CRITICAL Violation:
Blocking I/O in Tx"]
IORegistry -->|No| CallGraph{"Calls Local Helper Function?"}
CallGraph -->|Yes| HelperWalk["Walk Helper Function Body"]
HelperWalk --> IORegistry
CallGraph -->|No| Safe
```
1. **Transaction Boundary Detection (`tx_detector.go`):** Identifies both closure-based transactions (`pgx.BeginFunc`, `ExecuteTx`) and explicit block scopes (`pool.Begin` through `tx.Commit`/`tx.Rollback`).
2. **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).
3. **Interprocedural Call Graph (`callgraph_walker.go`):** Follows local function calls made within the transaction block to detect hidden I/O invocations inside helpers.
---
## 4. Vulnerability & Risk Taxonomy
| 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** |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: HTTP Call Inside `BeginFunc`
```go
// 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
})
}
```
### Example 2: Helper Function with Hidden I/O
```go
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)
}
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Solution 1: Transactional Outbox Pattern (Standard)
```go
// 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
})
}
```
### Solution 2: Pre-fetching or Post-Execution I/O
```go
// 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
})
}
```
---
## 7. How to Suppress (Ignore Directives)
For test harnesses, benchmark harnesses, or intentional latency injection:
```go
// argus:ignore ARGUS-A08 load testing artificial latency injection
time.Sleep(200 * time.Millisecond)
```
Alternatively, use the identifier alias:
```go
// argus:ignore TX_EXTERNAL_IO verified test harness mock dispatch
resp, err := http.Post(testServerURL, "application/json", body)
```
---
## 8. Configuration Reference (`.argus.yaml`)
Enable or configure this rule in `.argus.yaml`:
```yaml
rules:
ARGUS-A08:
enabled: true
```