# ARGUS-A09: PostgreSQL Advisory Lock Leaks & Namespace Collisions
> **Rule Code:** `ARGUS-A09`
> **Identifier:** `ADVISORY_LOCK`
> **Severity:** `HIGH / CRITICAL` (Permanent Lock Leak & Deadlock Convoy Blocker)
> **Category:** `Concurrency, Deadlock Prevention & Namespace Hygiene`
> **Target Standards:** CWE-662 (Improper Synchronization), CWE-833 (Deadlock), OWASP ASVS v4.0.3/v5.0 §V1.4.3
---
## 1. Overview & Core Invariant
1. **Mandatory Transaction-Scoped Locks:** Application concurrency control utilizing PostgreSQL Advisory Locks **must use transaction-level lock functions** (`pg_advisory_xact_lock`, `pg_advisory_xact_lock_shared`, `pg_try_advisory_xact_lock`, `pg_try_advisory_xact_lock_shared`). Session-level advisory locks (`pg_advisory_lock`, `pg_try_advisory_lock`) are strictly prohibited on connections managed by connection pools (`*pgxpool.Pool`).
2. **Structured Namespace Identifiers:** Lock identifiers must use a single 64-bit `bigint` or a 32-bit integer pair (`(class_id, object_id)`) sourced from a registered constant registry or deterministic hash function (`argus.LockKey(domain, resource)`). Arbitrary hardcoded integer literals (e.g. `1`, `42`, `100`) are prohibited to prevent cross-feature lock collisions.
---
## 2. Technical Grounding & PostgreSQL Engine Realities
### 2.1. Shared Memory Lock Table & The Connection Pool Leak Disaster
PostgreSQL stores advisory locks directly in kernel shared memory (`LockMethodData`). The lifecycle differences between session and transaction locks govern application stability:
- **`pg_advisory_xact_lock`:** Tied to the current PostgreSQL Transaction ID (`xid`). The engine automatically releases the lock when the transaction commits or aborts (`ROLLBACK`), even during application crashes or network dropouts.
- **`pg_advisory_lock` (Session-Level):** Tied to the physical backend TCP connection. The lock is **never automatically released** until the connection is closed or `pg_advisory_unlock()` is explicitly invoked with the identical ID.
In connection-pooled architectures (`pgxpool.Pool`), if a worker acquires `pg_advisory_lock(101)` and panics or fails to unlock, the physical connection returns to the pool **still holding lock 101 in PostgreSQL memory**. All subsequent workers requesting lock 101 block indefinitely, creating a permanent deadlock convoy (CWE-833).
```mermaid
flowchart TD
subgraph DEADLOCK ["Session Lock Pool Leak (VULNERABLE)"]
direction TB
WorkerA["Worker A: SELECT pg_advisory_lock(101)"] --> PG["PostgreSQL Lock Table (Lock 101 Held)"]
WorkerA --> Panic["Worker A Panics or Returns Early (No Unlock)"]
Panic --> ReturnPool["Connection Returned to pgxpool (STILL HOLDING LOCK!)"]
ReturnPool --> WorkerB["Worker B: Requests Lock 101 -> BLOCKED FOREVER"]
WorkerB --> WorkerC["Worker C: Requests Lock 101 -> BLOCKED FOREVER"]
WorkerC --> Outage["Cluster Processing Freeze (CWE-662 / CWE-833)"]
end
subgraph SAFE ["Transaction-Level Advisory Lock (COMPLIANT)"]
direction TB
Worker1["Worker 1: SELECT pg_advisory_xact_lock($1)"] --> PGX["Transaction Active (Lock Held)"]
Worker1 --> Fail["Worker 1 Panics / Returns Error"]
Fail --> Abort["Transaction Aborts (ROLLBACK)"]
Abort --> AutoRelease["PostgreSQL Kernel Automatically Releases Lock (SAFE)"]
end
```
### 2.2. Dedicated Connection Exception for Leader Election
Session-level advisory locks are permitted only for distributed leader election when executed on an explicitly dedicated connection (`pool.Acquire(ctx)`), never shared with standard application request pools, and explicitly closed on process termination.
---
## 3. How Argus Detects Violations (Static Analysis Architecture)
Argus combines PostgreSQL AST query inspection with Go helper argument validation:
```mermaid
flowchart LR
Scan["Scan Go Files
(Exclude _test.go)"] --> Selectors{"DB Query Call or
Advisory Helper?"}
Selectors -->|DB Query| ParseSQL["lock_ast_check.go:
pg_query_go AST Inspection"]
Selectors -->|Helper Call| CheckArgs["namespace_check.go:
Validate Lock Names"]
ParseSQL --> SessionCheck{"Uses Session Lock?
(pg_advisory_lock, etc.)"}
ParseSQL --> MagicCheck{"Uses Hardcoded Magic Int?
(e.g. pg_advisory_xact_lock(1))"}
SessionCheck -->|Yes| ReportSession["Report CRITICAL Violation:
Forbidden Session Lock"]
MagicCheck -->|Yes| ReportMagic["Report HIGH Violation:
Hardcoded Lock Identifier"]
CheckArgs --> EmptyCheck{"Lock Name Empty String?"}
EmptyCheck -->|Yes| ReportEmpty["Report HIGH Violation:
Empty Advisory Lock Name"]
SessionCheck -->|No| Pass["Pass (Safe Transaction Lock)"]
MagicCheck -->|No| Pass
EmptyCheck -->|No| Pass
```
1. **AST Function Inspection (`lock_ast_check.go`):** Identifies `FuncCall` nodes in PostgreSQL AST matching forbidden session lock functions (`pg_advisory_lock`, `pg_try_advisory_lock`, `pg_advisory_unlock`).
2. **Hardcoded Integer Constant Detection (`lock_ast_check.go`):** Detects raw integer constants (`c.GetIval()`) passed directly to lock functions without bind parameters.
3. **Helper Namespace Validation (`namespace_check.go`):** Validates calls to `argus.WithAdvisoryLock` and `argus.ExecuteLockedTx`, preventing empty lock names.
---
## 4. Vulnerability & Risk Taxonomy
| Failure Mode | Technical Impact | Risk Severity |
| :-------------------------- | :----------------------------------------------------------------------------- | :------------ |
| **Session Lock in Pool** | Panic or uncaught error permanently holds lock in pool, blocking all workers. | **CRITICAL** |
| **Hardcoded Magic Numbers** | Collisions across unrelated domain modules lead to accidental lock starvation. | **HIGH** |
| **Empty Helper String** | Lock fails to isolate specific resource, leading to race conditions. | **HIGH** |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: Session-Level Lock on Connection Pool
```go
// VIOLATION: Session lock leaks if function returns error or panics
func ProcessPayout(ctx context.Context, pool *pgxpool.Pool, payoutID int64) error {
// Flagged: forbidden session-level advisory lock "pg_advisory_lock"
_, err := pool.Exec(ctx, "SELECT pg_advisory_lock($1)", payoutID)
if err != nil {
return err
}
defer pool.Exec(ctx, "SELECT pg_advisory_unlock($1)", payoutID)
return executePayout(ctx, payoutID)
}
```
### Example 2: Hardcoded Magic Number Lock Key
```go
// VIOLATION: Hardcoded literal key risks cross-feature namespace collision
func SyncDailyExchangeRates(ctx context.Context, pool *pgxpool.Pool) error {
// Flagged: hardcoded integer advisory lock key in SQL
_, err := pool.Exec(ctx, "SELECT pg_advisory_xact_lock(42)")
return err
}
```
### Example 3: Empty Lock Identifier in Helper
```go
// VIOLATION: Empty lock name bypasses concurrency isolation
func ReconcileAccounts(ctx context.Context, tx pgx.Tx) error {
// Flagged: empty advisory lock name
return argus.WithAdvisoryLock(ctx, tx, "", true, func() error {
return executeReconciliation(ctx, tx)
})
}
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Solution 1: Parameterized Transaction Lock (Standard)
```go
// COMPLIANT: Automatically released when transaction ends
func ProcessPayout(ctx context.Context, pool *pgxpool.Pool, payoutKey int64) error {
return pool.BeginFunc(ctx, func(tx pgx.Tx) error {
const query = "SELECT pg_advisory_xact_lock($1)"
if _, err := tx.Exec(ctx, query, payoutKey); err != nil {
return err
}
return executePayout(ctx, tx)
})
}
```
### Solution 2: Two-Argument 32-Bit Namespace Lock
```go
// COMPLIANT: Class ID and Object ID namespace isolation
func LockTenantResource(ctx context.Context, tx pgx.Tx, classID, objectID int32) error {
const query = "SELECT pg_advisory_xact_lock($1, $2)"
_, err := tx.Exec(ctx, query, classID, objectID)
return err
}
```
### Solution 3: Structured Helper with Namespaced Lock Key
```go
// COMPLIANT: Explicit domain-namespaced lock identifier
func ProcessOrder(ctx context.Context, tx pgx.Tx, orderID string) error {
lockKey := fmt.Sprintf("orders:processing:%s", orderID)
return argus.WithAdvisoryLock(ctx, tx, lockKey, true, func() error {
return executeOrderProcessing(ctx, tx, orderID)
})
}
```
---
## 7. How to Suppress (Ignore Directives)
For long-lived cluster leader election or specialized single-connection runners:
```go
// argus:ignore ARGUS-A09 dedicated non-pooled connection leader election
_, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", leaderKey)
```
Alternatively, use the identifier alias:
```go
// argus:ignore ADVISORY_LOCK verified dedicated connection single-worker lease
_, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", leaseID)
```
---
## 8. Configuration Reference (`.argus.yaml`)
Enable or configure this rule in `.argus.yaml`:
```yaml
rules:
ARGUS-A09:
enabled: true
```