Skip to content

AI Restyle: route mutations through _job_lock (today bypassed) #37

@vansteenbergenmatisse

Description

@vansteenbergenmatisse

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions