Files
edict/dashboard
voidborne-d 714f442e31 fix: eliminate TOCTOU race in task mutation paths
Background dispatch threads, the periodic scheduler scan, and HTTP
handlers all followed a load_tasks() → modify → save_tasks() pattern
without any mutual exclusion.  The atomic_json_write only protects the
write itself; it does NOT prevent a concurrent reader from loading a
stale snapshot and overwriting another thread's changes.

Scenario (before this fix):
  1. HTTP handler calls load_tasks() — gets snapshot A
  2. Dispatch thread calls load_tasks() — gets the same snapshot A
  3. HTTP handler modifies task X, calls save_tasks() — writes A'
  4. Dispatch thread modifies task Y, calls save_tasks() — writes A''
     (based on A, not A'), silently losing the HTTP handler's changes
     to task X

This is a classic TOCTOU (Time-of-Check-Time-of-Use) race.  The project
already has atomic_json_update() in file_lock.py that holds an exclusive
lock for the entire read-modify-write cycle, but none of the task
mutation paths used it.

Fix:
- Add modify_tasks(modifier) wrapper around atomic_json_update +
  refresh trigger
- Add modify_task(task_id, updater) convenience wrapper for single-task
  mutations
- Convert _update_task_scheduler (called from dispatch daemon threads)
  to use modify_task
- Convert handle_scheduler_scan (periodic background scanner) to use
  modify_tasks, with dispatch side-effects deferred until after the
  lock is released
- Convert handle_scheduler_retry and handle_scheduler_rollback to use
  modify_task

HTTP handler paths (handle_task_action, handle_review_action, etc.)
still use load_tasks/save_tasks for now — they run in the main thread
and are lower risk — but can be migrated incrementally.

Add 17 regression tests covering: atomic API correctness, scheduler
update persistence, scan stall detection, concurrent write survival
(the actual race), source-level audit of critical paths, and backward
compatibility of load_tasks/save_tasks.
2026-04-22 06:00:26 +00:00
..