Surfaced by the Codex adversarial security audit on PR #35. One of 4 deferred MEDIUMs.
Where
backend/app/restyle/pipeline.py:33-49 (helpers that touch jobs[job_id]):
def _ensure_job(jobs, job_id):
if job_id not in jobs:
jobs[job_id] = {"status": "processing", ...}
return jobs[job_id]
def _log(jobs, job_id, line, pct=None):
job = _ensure_job(jobs, job_id)
job["logs"].append(line)
if pct is not None:
job["progress_pct"] = pct
And the final commit at restyle/pipeline.py:119-132:
job = jobs[job_id]
job["result"] = {...}
job["status"] = "completed"
...
except Exception as exc:
job = _ensure_job(jobs, job_id)
job["status"] = "failed"
job["logs"].append(f"❌ {exc}")
What's wrong
Short-form's job mutations are gated by _job_lock() + _atomic_write_json() (see backend/app/main.py). AI Restyle bypasses that machinery — it writes jobs[job_id] directly.
Today's risk is consistency-only, not corruption:
- Exactly one writer per job_id (the BackgroundTask coroutine spawned by
POST /api/restyle).
- The reader path (
GET /api/restyle/{job_id}) is read-only and projects through a Pydantic model that tolerates partial fields.
So nothing exploitable lands in v1. But the moment a second writer joins (retry button, cancel route, websocket progress push) the pattern breaks, and TOCTOU between read-modify-write at lines 119-127 becomes a real race.
Severity
MEDIUM. Defensible deferral — the consistency-only risk is real but bounded.
Suggested fix
Extract a tiny app/core/job_store.py (already telegraphed in CLAUDE.md Convention #6) and route both Short-form and AI Restyle through it. Minimum API:
class JobStore:
async def update(self, job_id: str, **patch) -> None # locks + atomic write
async def append_log(self, job_id: str, line: str, pct: int | None = None) -> None
def read(self, job_id: str) -> dict | None
Land it as part of the router-split work already deferred in ROADMAP.md ("Phase 1 leftovers" table).
References
Surfaced by the Codex adversarial security audit on PR #35. One of 4 deferred MEDIUMs.
Where
backend/app/restyle/pipeline.py:33-49(helpers that touchjobs[job_id]):And the final commit at
restyle/pipeline.py:119-132:What's wrong
Short-form's job mutations are gated by
_job_lock()+_atomic_write_json()(seebackend/app/main.py). AI Restyle bypasses that machinery — it writesjobs[job_id]directly.Today's risk is consistency-only, not corruption:
POST /api/restyle).GET /api/restyle/{job_id}) is read-only and projects through a Pydantic model that tolerates partial fields.So nothing exploitable lands in v1. But the moment a second writer joins (retry button, cancel route, websocket progress push) the pattern breaks, and TOCTOU between read-modify-write at lines 119-127 becomes a real race.
Severity
MEDIUM. Defensible deferral — the consistency-only risk is real but bounded.
Suggested fix
Extract a tiny
app/core/job_store.py(already telegraphed inCLAUDE.mdConvention #6) and route both Short-form and AI Restyle through it. Minimum API:Land it as part of the router-split work already deferred in ROADMAP.md ("Phase 1 leftovers" table).
References
openshorts/core/job_store.pyrowbackend/app/main.py(_job_lock+_atomic_write_json)