-
Notifications
You must be signed in to change notification settings - Fork 0
ARGUS A02
Rule Code:
ARGUS-A02Identifier:MISSING_DEFER_CLOSESeverity:HIGH(Connection Leak & Pool Starvation Blocker) Category:Resource & Connection LifecycleTarget 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
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.
When an application invokes Query(), the PostgreSQL backend opens an active server-side Portal:
-
Physical Socket Pinning: An active
pgx.Rowsobject holds an exclusive lease on an underlying TCP socket connection checked out from*pgxpool.Pool. -
Wire Protocol Handshake: Until the client explicitly transmits the wire protocol
Close (C)andSync (S)messages, the PostgreSQL backend process holds active buffer locks and stays inidle in transactionor unclosed portal state. -
Connection Starvation Cascade: If the function exits early (e.g. during
rows.Scan(&val)error) withoutdefer rows.Close(), the TCP connection remains orphaned. Within seconds under concurrent production traffic, the pool'sMaxConnslimit is exhausted, causing a catastrophic backend freeze and HTTP 503 / gateway timeout errors (ref:jackc/pgxissue #2153).
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
Argus examines function declarations (*ast.FuncDecl) and closures (*ast.FuncLit) using dedicated lexical AST traversal:
flowchart LR
Scan["Detect Query() Call"] --> CheckRet{"Is Rows Returned<br/>to Caller?"}
CheckRet -->|Yes| ReportOwnership["Report Violation:<br/>Ownership Transfer Forbidden"]
CheckRet -->|No| CheckSafe{"Is Safely Deferred or<br/>Consumed by Helper?"}
CheckSafe -->|Yes| Pass["Pass (Safe)"]
CheckSafe -->|No| ReportLeak["Report Violation:<br/>Missing defer rows.Close()"]
-
Direct Return Detection: Flags any
return db.Query(...)that directly passes unconsumed rows to the caller. -
Variable Ownership Tracking: Verifies if the variable assigned to the result of
Query()appears in any subsequent*ast.ReturnStmt. -
Lexical Defer Verification: Scans the enclosing function body for
defer rows.Close()or closuredefer func() { rows.Close() }(). -
Auto-Closing Helper Whitelist: Automatically accepts official
pgxstreaming helpers that guarantee internal cleanup:pgx.CollectRows(rows, ...)pgx.CollectOneRow(rows, ...)pgx.ForEachRow(rows, ...)
| 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 |
// 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
}// 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
}// 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
}// 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
}// 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])
}// 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
}For mock generators or test fixtures where resource management is intentionally tested:
// argus:ignore ARGUS-A02 test mock stream generator
rows, err := dbpool.Query(ctx, testQuery)Alternatively, use the identifier alias:
// argus:ignore MISSING_DEFER_CLOSE simulated unclosed connection fixture
rows, err := dbpool.Query(ctx, testQuery)Enable or disable this rule globally in .argus.yaml:
rules:
ARGUS-A02:
enabled: true