T09 · Insecure Skill Coding Practices
- Location
scripts/clankerhive.py:213- Finding
Non-Atomic Task Claiming Permits Duplicate Execution
- Content
View full analysis
Vulnerability Details
File Location:
scripts/clankerhive.py, lines 213–227
Vulnerability Type: Race condition in task deduplication
Risk Level: Mediumpython row = conn.execute( "SELECT status, owner FROM task_state WHERE task_id = ?", (args.task_id,) ).fetchone() if row and row["status"] == "claimed": print(f"already-claimed by {row['owner']}") conn.close() sys.exit(1) conn.execute( "INSERT OR REPLACE INTO task_state " "(task_id, status, owner, started_at, updated_at) VALUES (?, 'claimed', ?, ?, ?)", (args.task_id, owner, now, now), ) conn.commit() conn.close() print("ok")Technical Analysis
Task acquisition is implemented as a check-then-write sequence. The initial
SELECTand subsequentINSERT OR REPLACEdo not execute within a transaction that excludes competing writers for the entire operation.Two processes can therefore read the same task state before either process commits its claim. If the task does not exist, or is not currently marked as claimed, both processes can proceed to the write. The use of
INSERT OR REPLACEcauses the later writer to replace the earlier claim instead of rejecting the conflicting acquisition.SQLite serializes the individual writes, but that does not make the logical read-modify-write operation atomic. Consequently, more than one caller can receive
okand believe it exclusively owns the task.Attack Path
- Two local sessions invoke
claim-taskconcurrently with the same task ID. - Both sessions execute the
SELECTbefore either claim is committed. - Both observe that no active claim prevents acquisition.
- The first session writes and commits its claim.
- The second session executes
INSERT OR REPLACE, overwriting the first claim. - Both sessions print
okand execute the supposedly deduplicated operation. - Only the final writer remains recorded as the task owner, concealing the d ...[truncated 618 chars]
- Two local sessions invoke
- Remediation
View remediation
Remediation Suggestions
Replace the check-then-write sequence with a single atomic conditional operation. For tasks that must never be claimed twice, use an insertion that does not replace an existing row:
python cur = conn.execute( "INSERT INTO task_state " "(task_id, status, owner, started_at, updated_at) " "VALUES (?, 'claimed', ?, ?, ?) " "ON CONFLICT(task_id) DO NOTHING", (args.task_id, owner, now, now), ) conn.commit() if cur.rowcount != 1: # Claim was not acquired. sys.exit(1)If completed tasks are intentionally reclaimable, execute the state check and a conditional update inside a
BEGIN IMMEDIATEtransaction. The update should include the expected prior state in itsWHEREclause, and success must be determined from the affected-row count.Avoid
INSERT OR REPLACEfor claims because it can silently remove and recreate existing task state. Add a concurrency test that starts multiple processes against the same task ID and verifies that exactly one process reports successful acquisition.
