# ARGUS-A18: Mandatory rows.Err() Check After Cursor Iteration
> **Rule Code:** `ARGUS-A18`
> **Identifier:** `MISSING_ROWS_ERR_CHECK`
> **Severity:** `HIGH` (Silent Data Truncation & Partial Batch Execution)
> **Category:** `Data Integrity, Stream Reliability & Error Handling`
> **Target Standards:** CWE-391 (Unchecked Error Condition), CWE-662 (Improper Synchronization / Partial Processing), OWASP ASVS v4.0.3/v5.0 §V5.3.1, §V7.1.1
---
## 1. Overview & Core Invariant
Immediately after completing a database cursor loop (**`for rows.Next() { ... }`**), application code **must inspect `rows.Err()`** before returning, processing, or acting upon the collected dataset.
Ignoring `rows.Err()` or returning data directly after the loop under the assumption that `rows.Next()` only returns `false` upon reaching EOF is strictly forbidden.
Automatic exemption is granted when using official auto-closing collection helpers that handle stream errors internally:
- **`pgx.CollectRows(rows, ...)`**
- **`pgx.CollectOneRow(rows, ...)`**
- **`pgx.ForEachRow(rows, ...)`**
---
## 2. Technical Grounding & PostgreSQL Engine Realities
### 2.1. TCP Socket Streaming & Dual Exit Condition of `rows.Next()`
In PostgreSQL Extended Query Protocol v3.0, query results are streamed asynchronously over TCP sockets as individual `DataRow (D)` message frames.
The driver method `rows.Next()` returns boolean **`false`** under two fundamentally opposite conditions:
1. **Condition A (Clean EOF / Normal Completion):** All rows have been transmitted by PostgreSQL and received by the client, concluded by a `CommandComplete (C)` protocol message.
2. **Condition B (Abnormal Stream Disruption):** An unrecoverable error occurred mid-stream:
- Network socket disconnect (_TCP RST_ / packet drop).
- Context cancellation or deadline exceeded.
- Column type decoding failure on a specific row.
- Server-side `statement_timeout` triggered by PostgreSQL engine while streaming thousands of rows.
### 2.2. The Silent Dataset Truncation Disaster (CWE-391)
Without inspecting `rows.Err()`, Go applications cannot differentiate between Condition A and Condition B. The application treats truncated partial data as a complete dataset and proceeds with business logic.
```mermaid
flowchart TD
subgraph S1 ["5,000 Records Streamed over TCP from PostgreSQL"]
D1["DataRow 1..1250 (Received OK)"] --> Drop["TCP Packet Drop / statement_timeout at Row 1251!"]
end
Drop --> NextRet["rows.Next() returns false"]
subgraph HAZ ["Without rows.Err() (HAZARDOUS - ARGUS-A18 Violation)"]
NextRet --> Ignored["No rows.Err() check"]
Ignored --> Process["Process 1,250 items as COMPLETE result"]
Process --> Disaster["DISASTER: 3,750 Records Silently Lost / Truncated!"]
end
subgraph COMP ["With rows.Err() Check (COMPLIANT)"]
NextRet --> Check["if err := rows.Err(); err != nil"]
Check --> Detect["Detects Stream Disruption"]
Detect --> Rollback["Transaction ROLLBACK & Error Alarm Logged"]
end
```
---
## 3. How Argus Detects Violations (Static Analysis Architecture)
Argus inspects Go AST control flows across enclosing block statements:
```mermaid
flowchart LR
Scan["Scan Go Source AST
(Exclude _test.go)"] --> LoopFind["loop_inspector.go:
Find ForStmt with .Next() Condition"]
LoopFind --> ReceiverCheck["helper_whitelist.go:
Verify Receiver is Database Rows"]
ReceiverCheck -->|Non-DB Iterator| Pass["Pass (Non-DB Iterator)"]
ReceiverCheck -->|Database Rows| ErrCheck["err_checker.go:
Inspect Subsequent Block Stmts"]
ErrCheck --> HasErr{"Calls rows.Err() before
return or block exit?"}
HasErr -->|Yes| Pass["Pass (Compliant Error Handling)"]
HasErr -->|No| Ignored{"Directives:
// argus:ignore ARGUS-A18?"}
Ignored -->|Yes| Pass["Pass (Suppressed)"]
Ignored -->|No| Report["Report HIGH Violation:
missing rows.Err() check"]
```
1. **Loop Inspector (`loop_inspector.go`):** Identifies `*ast.ForStmt` where the condition invokes `.Next()` on a database rows receiver.
2. **Helper Whitelist & Type Guard (`helper_whitelist.go`):** Ensures only database row iterators (`pgx.Rows`, `sql.Rows`) are targeted, ignoring benchmark helpers (`pb.Next()`).
3. **Error Checker (`err_checker.go`):** Validates that subsequent statements in the enclosing block inspect `rows.Err()` before any unconditional `return` statement.
---
## 4. Vulnerability & Risk Taxonomy
| Failure Mode | Technical Impact | Risk Severity |
| :------------------------------------- | :----------------------------------------------------------------- | :------------------------ |
| **Missing `rows.Err()` Check** | Silent dataset truncation during network drop or statement timeout | `HIGH` (CWE-391, CWE-662) |
| **Early Return Before `rows.Err()`** | Premature function exit with partial dataset | `HIGH` |
| **Premature Mutation on Partial Data** | Mutating secondary tables using incomplete query results | `CRITICAL` |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: Missing `rows.Err()` Before Return
```go
// NON-COMPLIANT: rows.Err() omitted after loop
func ListActiveUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
rows, err := pool.Query(ctx, "SELECT id, email FROM users WHERE active = true")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Email); err != nil {
return nil, err
}
users = append(users, u)
}
// HAZARDOUS: If stream dropped at row 50, partial list is returned as complete!
return users, nil
}
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Pattern 1: Immediate `rows.Err()` Check (Recommended Standard)
```go
// COMPLIANT: Explicit rows.Err() check immediately after loop
func ListActiveUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
rows, err := pool.Query(ctx, "SELECT id, email FROM users WHERE active = true")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Email); err != nil {
return nil, err
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("user query stream interrupted: %w", err)
}
return users, nil
}
```
### Pattern 2: Modern `pgx.CollectRows` Helper
```go
// COMPLIANT: pgx.CollectRows handles rows.Err() and rows.Close() internally
func ListActiveUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
rows, err := pool.Query(ctx, "SELECT id, email FROM users WHERE active = true")
if err != nil {
return nil, err
}
defer rows.Close()
return pgx.CollectRows(rows, pgx.RowToStructByName[User])
}
```
---
## 7. How to Suppress (Ignore Directives)
In specialized telemetry or best-effort partial stream consumption:
```go
// argus:ignore ARGUS-A18 telemetry sampler allows best-effort partial stream
for rows.Next() {
// ...
}
```
Or using shortcode:
```go
// argus:ignore-a18 best effort log sampler
for rows.Next() {
// ...
}
```
---
## 8. Configuration Reference (`.argus.yaml`)
```yaml
rules:
ARGUS-A18:
enabled: true
severity: HIGH
```