# ARGUS-A02: Missing Defer Rows Close
> **Rule Code:** `ARGUS-A02`
> **Identifier:** `MISSING_DEFER_CLOSE`
> **Severity:** `HIGH` (Connection Leak & Pool Starvation Blocker)
> **Category:** `Resource & Connection Lifecycle`
> **Target Standards:** CWE-772 (Missing Release of Resource after Effective Lifetime), CWE-404 (Improper Resource Shutdown), OWASP ASVS v4.0.3/v5.0 §V1.4.3
---
## 1. Overview & Core Invariant
Every database call that produces a `pgx.Rows` instance (`dbpool.Query`, `tx.Query`, `conn.Query`) **must** immediately register resource release through `defer rows.Close()` within the **same lexical scope**, or be consumed by an auto-closing helper (`pgx.CollectRows`, `pgx.CollectOneRow`, `pgx.ForEachRow`).
Transferring ownership of active `pgx.Rows` outside the constructing function (such as returning `pgx.Rows` to a caller service or HTTP handler) is strictly prohibited. The opening function must fully scan and map all rows into domain structs or values before returning.
---
## 2. Technical Grounding & PostgreSQL Engine Realities
When an application invokes `Query()`, the PostgreSQL backend opens an active server-side **Portal**:
1. **Physical Socket Pinning:** An active `pgx.Rows` object holds an exclusive lease on an underlying TCP socket connection checked out from `*pgxpool.Pool`.
2. **Wire Protocol Handshake:** Until the client explicitly transmits the wire protocol `Close (C)` and `Sync (S)` messages, the PostgreSQL backend process holds active buffer locks and stays in `idle in transaction` or unclosed portal state.
3. **Connection Starvation Cascade:** If the function exits early (e.g. during `rows.Scan(&val)` error) without `defer rows.Close()`, the TCP connection remains orphaned. Within seconds under concurrent production traffic, the pool's `MaxConns` limit is exhausted, causing a catastrophic backend freeze and HTTP 503 / gateway timeout errors (ref: `jackc/pgx` issue #2153).
```mermaid
flowchart TD
subgraph SAFE ["Lexical defer rows.Close() (SAFE)"]
direction TB
App1["Go App: pool.Query()"] -->|"Acquires TCP Connection"| PG1["PostgreSQL: Opens Portal"]
App1 -->|"defer rows.Close() registered"| Defer1["Scope Exit Guaranteed"]
Defer1 -->|"Sends Close(C) + Sync(S)"| ReturnConn["Connection Returned to Pool (HEALTHY)"]
end
subgraph LEAK ["Missing Defer / Naked Close (CATASTROPHIC RISK)"]
direction TB
App2["Go App: pool.Query()"] -->|"Acquires TCP Connection"| PG2["PostgreSQL: Opens Portal"]
App2 -->|"rows.Scan() Fails -> Early Return"| Exit2["Function Exits Without Close()"]
Exit2 -->|"Connection Never Returned"| Starve["Pool Starvation & Production Deadlock (CWE-772)"]
end
```
---
## 3. How Argus Detects Violations (Static Analysis Architecture)
Argus examines function declarations (`*ast.FuncDecl`) and closures (`*ast.FuncLit`) using dedicated lexical AST traversal:
```mermaid
flowchart LR
Scan["Detect Query() Call"] --> CheckRet{"Is Rows Returned
to Caller?"}
CheckRet -->|Yes| ReportOwnership["Report Violation:
Ownership Transfer Forbidden"]
CheckRet -->|No| CheckSafe{"Is Safely Deferred or
Consumed by Helper?"}
CheckSafe -->|Yes| Pass["Pass (Safe)"]
CheckSafe -->|No| ReportLeak["Report Violation:
Missing defer rows.Close()"]
```
1. **Direct Return Detection:** Flags any `return db.Query(...)` that directly passes unconsumed rows to the caller.
2. **Variable Ownership Tracking:** Verifies if the variable assigned to the result of `Query()` appears in any subsequent `*ast.ReturnStmt`.
3. **Lexical Defer Verification:** Scans the enclosing function body for `defer rows.Close()` or closure `defer func() { rows.Close() }()`.
4. **Auto-Closing Helper Whitelist:** Automatically accepts official `pgx` streaming helpers that guarantee internal cleanup:
- `pgx.CollectRows(rows, ...)`
- `pgx.CollectOneRow(rows, ...)`
- `pgx.ForEachRow(rows, ...)`
---
## 4. Vulnerability & Risk Taxonomy
| Failure Mode | Technical Impact | Risk Severity |
| :---------------------------------------------- | :---------------------------------------------------------------------------------------- | :------------ |
| **Missing `defer rows.Close()`** | Unclosed server-side portals leak TCP socket connections on any scan or iteration error. | **CRITICAL** |
| **Non-Deferred `rows.Close()` at Function End** | Any early return inside `rows.Next()` bypasses the closing call completely. | **HIGH** |
| **Returning `pgx.Rows` to Caller** | Shifts cleanup responsibility to outer layers, creating widespread blind spots and leaks. | **HIGH** |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: Non-Deferred Close at Function End
```go
// VIOLATION: Calling rows.Close() without defer
func GetActiveUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
rows, err := pool.Query(ctx, "SELECT id, name FROM users WHERE active = true")
if err != nil {
return nil, err
}
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
// CATASTROPHIC: Early return bypasses rows.Close() below, leaking connection!
return nil, err
}
users = append(users, u)
}
rows.Close() // Too late: skipped on any Scan error!
return users, nil
}
```
### Example 2: Transferring Rows Ownership to Caller
```go
// VIOLATION: Returning pgx.Rows outside the constructing function
func QueryUsersRaw(ctx context.Context, pool *pgxpool.Pool) (pgx.Rows, error) {
rows, err := pool.Query(ctx, "SELECT id, name FROM users")
if err != nil {
return nil, err
}
// FORBIDDEN: Transfers resource management responsibility to caller
return rows, nil
}
```
### Example 3: Missing Close Entirely
```go
// VIOLATION: Rows object is never closed
func UserExists(ctx context.Context, pool *pgxpool.Pool, id string) (bool, error) {
rows, err := pool.Query(ctx, "SELECT 1 FROM users WHERE id = $1", id)
if err != nil {
return false, err
}
// Rows remains open permanently in connection pool!
return rows.Next(), nil
}
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Solution 1: Immediate `defer rows.Close()` (Standard)
```go
// COMPLIANT: defer rows.Close() registered immediately after error verification
func GetActiveUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
rows, err := pool.Query(ctx, "SELECT id, name FROM users WHERE active = true")
if err != nil {
return nil, err
}
defer rows.Close() // Guaranteed execution on ANY exit path
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, err
}
return users, nil
}
```
### Solution 2: Auto-Closing Helper (`pgx.CollectRows`)
```go
// COMPLIANT: pgx.CollectRows guarantees internal rows.Close() execution
func GetActiveUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
rows, err := pool.Query(ctx, "SELECT id, name FROM users WHERE active = true")
if err != nil {
return nil, err
}
return pgx.CollectRows(rows, pgx.RowToStructByName[User])
}
```
### Solution 3: Deferred Closure
```go
// COMPLIANT: Defer within closure wrapper
func QueryWithMetric(ctx context.Context, pool *pgxpool.Pool) error {
rows, err := pool.Query(ctx, "SELECT id FROM users")
if err != nil {
return err
}
defer func() {
rows.Close()
}()
// ...
return nil
}
```
---
## 7. How to Suppress (Ignore Directives)
For mock generators or test fixtures where resource management is intentionally tested:
```go
// argus:ignore ARGUS-A02 test mock stream generator
rows, err := dbpool.Query(ctx, testQuery)
```
Alternatively, use the identifier alias:
```go
// argus:ignore MISSING_DEFER_CLOSE simulated unclosed connection fixture
rows, err := dbpool.Query(ctx, testQuery)
```
---
## 8. Configuration Reference (`.argus.yaml`)
Enable or disable this rule globally in `.argus.yaml`:
```yaml
rules:
ARGUS-A02:
enabled: true
```