Repository navigation
[obvious-fleet] Backend: fail closed on missing upload-token secret; purge packet cache on delete; close delete/hydrate cache race - #250
Conversation
…t cache on delete Addresses the Greptile apex findings on PR #247. uploads.py: the token-only /v1/uploads routes registered on every storage backend, and _secret() verified against an empty HMAC key when UPLOAD_URL_HMAC_SECRET was unset, so anyone who knew a storage key could forge a GET token and read protected capture files. Routes now register only when a secret is configured, volume deployments reject a missing secret at startup with a clear error, VolumeStorage refuses an empty key, and the verifier fails closed at request time. Local tests opt in explicitly with UPLOAD_TOKENS_ALLOW_MISSING_SECRET=1. captures.py: DELETE removed only object-storage prefixes while workers publish packet copies to /vol/packet-cache/{capture_id}/{sha}. DELETE now purges the capture's whole shared-cache slot and logs a storage.warning naming any paths it could not remove instead of claiming a clean 204. hydrate_packet skips captures already tombstoned, so an in-flight worker cannot restore or re-publish the purged cache. Assisted-by: obvious agent
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
|
|
Independent review (Obvious, Autobuild + Max) Reviewed head Findings (ranked)1. Medium — Evidence: the flag's only consumer in the entire tree is the test that exercises it ( Smallest fix: drop the setting (the flag branches in 2. Low — the volume Evidence: this message is raised by the volume branch when Smallest fix: drop that sentence from this message (or say "not applicable to volume storage"). 3. Low — purge survivor report lists directories among files — Evidence: Smallest fix: filter the reported list to files: What I verified (fresh clone at
|
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d79830c38c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if _capture_tombstoned(state, capture_id): | ||
| return |
There was a problem hiding this comment.
Serialize the tombstone check with cache hydration
A worker can pass this tombstone check while DELETE is still in progress, then continue through _copy_packet_cache or download already-buffered packet bytes after the delete purges the cache and writes the tombstone; its later _publish_packet_cache can recreate the deleted capture's files. The check needs to be coordinated with deletion or repeated immediately before publishing so an in-flight worker cannot restore the cache.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed on the branch (fix 9135675, format-only c9230d0; head c9230d0 after merging alpha in e2c942d). hydrate_packet now re-checks _capture_tombstoned immediately before _publish_packet_cache — the entry check ran before the tombstone existed — and the DELETE route writes tomb:{capture_id} before calling purge_packet_cache, so a publish that raced the entry check lands inside the purge instead of after it. Each side is pinned by a regression test that fails against the old code: test_delete_mid_hydrate_does_not_republish_the_cache (delete interleaved mid-download; the finishing hydrate used to publish the slot right back) and test_delete_tombstones_before_purging_the_cache (the tombstone must already be visible when the cache purge runs).
…se a real secret Review follow-up on PR #250 (independent review, finding 1 + 2; lead agreed). The flag existed so one test could exercise empty-key verification, but it was unguarded config surface: set on any deployment, it re-registered the token routes and made _secret() verify against b"" again. Every other test already opts in with a real UPLOAD_URL_HMAC_SECRET (conftest.py), so the flag is gone entirely: - config.py: drop the Settings field. - uploads.py: routes_enabled() and _secret() lose the flag branch; the startup log line no longer advertises it. - storage_volume.py: the empty-key comment no longer mentions it. - config.py resolve_storage: the volume RuntimeError no longer suggests setting the flag on non-volume backends, where it could never help — the reader of that error can never use it. - test_uploads_token_gate.py: the empty-key test is replaced by test_local_test_secret_serves_token_routes, which sets a test secret and mints with the same key. No empty-key path exists or is exercised. The empty-key regression tests (routes absent without a secret; forged empty-key token gets 404) are unchanged and still pass. Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
…bs-backend-security # Conflicts: # backend/server/orchestrator/artifacts.py
Codex P1 on PR #250 (artifacts.py:236): a hydrate that passed the entry tombstone check while DELETE was still running published the packet cache slot back after DELETE purged it, restoring the deleted capture's files. Two-sided fix: hydrate_packet re-checks the tombstone immediately before _publish_packet_cache, and DELETE now writes the tombstone before it purges the cache, so a publish that raced the entry check lands inside the purge instead of after it. Each side is pinned by a regression test that fails against the old code.
|
Note Review skipped Codemetrics is installed on this repository, but it hasn't been added to your team yet. To enable AI code reviews for this repo, go to your dashboard and add BaseScanning to your team's repositories. |
There was a problem hiding this comment.
💡 Codex Review
BaseScanning/backend/server/orchestrator/artifacts.py
Lines 253 to 254 in c9230d0
The final _capture_tombstoned check and _publish_packet_cache call are still separate operations, so a worker can observe no tombstone, then DELETE can write the tombstone and finish purge_packet_cache, and the worker can publish immediately afterward, recreating the deleted capture's files. Fresh evidence in this revision is that the new guard only moves the race to the check at line 253; it does not coordinate that check with publication. Use a cross-process lock or make publication conditional/atomic with the tombstone state.
BaseScanning/backend/server/api/routes/captures.py
Lines 218 to 228 in c9230d0
When purge_packet_cache returns surviving paths, this handler still removes the capture record, writes the tombstone, and returns 204. A subsequent DELETE cannot retry the purge because it exits at the record is None check, leaving deleted photos on the shared volume indefinitely; logging the warning does not provide cleanup. Persist a retryable cleanup task/state or fail the deletion before making it irreversible.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
[Critical impact] Hardens upload token verification and fixes delete-hydrate race. The PR needs a fix before merging because cache deletion can leave photos stored on the shared Modal volume.
|
| base = packet_cache_dir(capture_id) | ||
| if base is None or not base.is_dir(): | ||
| return 0, [] |
There was a problem hiding this comment.
purge_packet_cache checks the API container's current view of /vol without reloading the Modal volume. A warm API container can miss a cache directory that a worker created and committed after the container started. Then base.is_dir() returns false, the purge reports (0, []), and DELETE returns 204 while the cached photos remain stored.
Reload the shared volume before checking the directory, and commit successful deletions before reporting completion. Add a test with separate API and worker volume views; the current temporary-directory tests cannot catch this.
How this was verified: The API mounts the same named Modal volume as the workers but never reloads it before deletion, while workers explicitly reload and commit their own view around each stage.
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/server/orchestrator/artifacts.py
Line: 194-196
Comment:
**Deleted photos remain stored**
`purge_packet_cache` checks the API container's current view of `/vol` without reloading the Modal volume. A warm API container can miss a cache directory that a worker created and committed after the container started. Then `base.is_dir()` returns false, the purge reports `(0, [])`, and DELETE returns `204` while the cached photos remain stored.
Reload the shared volume before checking the directory, and commit successful deletions before reporting completion. Add a test with separate API and worker volume views; the current temporary-directory tests cannot catch this.
**How this was verified:** The API mounts the same named Modal volume as the workers but never reloads it before deletion, while workers explicitly reload and commit their own view around each stage.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
fleet-packet: burst10-land-bs-security
Stacked on #247 — fixes the two Greptile apex findings from that review, fixes the Codex P1 race found on review of this PR, and merges
alpha(Sam decided 2026-10-16 04:40Z thatalphais the end-to-end test branch where finished work lands) so the branch can be reviewed as mergeable. Changes are confined tobackend/.Human author: Sam Gu (sgu07966@gmail.com)
What changed and why
Finding 1 — P1 security: capture files bypass access checks (uploads)
[Greptile on
backend/server/api/routes/uploads.py:23] — the old code registered the token-only/v1/uploads/{token}routes on every storage backend and verified tokens against an empty HMAC key whenUPLOAD_URL_HMAC_SECRETwas unset. On the dev deploy (STORAGE_BACKEND=gcs, no secret) anyone who knew a storage key could mint a valid empty-keyop: "get"token and read the object — no bearer credential involved. New code fails closed at three layers: routes register only when a signing secret is configured (404 otherwise, with a startup log line), avolumedeployment without the secret refuses to start, and_secret()raises 503uploads_disabledas defense in depth. The interimUPLOAD_TOKENS_ALLOW_MISSING_SECRETescape hatch added during the first round of review was removed again at review recommendation (3728d1b1): its only consumer was one test, every other test opts in with a real secret, and if ever set it re-opened the exact forgery this PR fixes. No empty-key path exists.Finding 2 — P1 security: deleted photos remain stored (packet cache)
[Greptile on
backend/server/api/routes/captures.py:213] — the old DELETE removed only theuploads/,captures/andwork/object-storage prefixes, while workers copy and commit the full packet into/vol/packet-cache/{capture_id}/{sha}on the shared volume, so photo copies survived a204. New:purge_packet_cache(capture_id)removes the capture's entire cache slot; a partial purge is reported via astorage.warning(up to 20 surviving paths) while DELETE stays204because object storage — the system of record — is clean;hydrate_packetreturns immediately for a tombstoned capture so a worker mid-run after DELETE neither re-downloads nor re-publishes.Codex P1 — a delete racing a cache refresh could resurrect the deleted capture's files
[Codex on
backend/server/orchestrator/artifacts.py:236] — the old code checked the tombstone once at hydrate entry, so a worker that passed the check before DELETE wrote the tombstone continued through the download and its later_publish_packet_cacherecreated the deleted capture's files after the purge. Fixed on both sides:hydrate_packetre-checks_capture_tombstonedimmediately before_publish_packet_cache, so a delete landing mid-hydrate is the last word. The run's own working copy is unaffected; only the shared-cache publish is suppressed.tomb:{capture_id}before it purges, so a publish that raced the entry check lands inside the purge instead of after it. The old order (purge → tombstone) left that window open.Merged alpha
origin/alphaat72062663into the branch (merge commite2c942d0; normal merge, no rebase, no force-push).backend/server/orchestrator/artifacts.pyonly. fix(backend): address Greptile review findings on the orchestrator #251'spacket_keyamendment plumbing and this branch's tombstone guard both landed inhydrate_packet. Resolution keeps alpha's version for alpha's change (packet_keyparameter,manifest_keydefault, 4-arg_packet_file_jobs) and this branch's for its own (entry tombstone check via_capture_tombstoned), with the docstring combining both.How you verified it
All in
backend/of a fresh clone (~/work/burst10-land-bs-security) on the BaseScanning repo sandbox; branch headc9230d03:uv run --frozen --extra api pytest server/orchestrator/tests/test_hydrate_inputs.py server/api/tests/test_uploads_token_gate.py→ 17 passed (15 pre-existing + 2 new).artifacts.pyandcaptures.pystashed and tests kept, both new tests fail —test_delete_mid_hydrate_does_not_republish_the_cache(old code republished the purged cache slot, soassert not cache.exists()fails) andtest_delete_tombstones_before_purging_the_cache(old code purged before the tombstone existed). Restored, both pass.uv run --frozen --extra api pytest server/api/tests server/orchestrator/tests -q→ 352 passed.ruff check .→ all checks passed;ruff format --check .→ 489 files formatted;lint-imports→ contracts 5 kept / 0 broken;python -m server.contracts.export_schemas --check→ ok.uv run --frozen --extra api pytest -q→ 1372 passed, 19 skipped, 5 failed, 14 errors (6:24). The 5 failures (tests/test_ssr_overlap_check.py, 4×server/eval/tests/test_gates.py) are the same pre-existing set already verified identical on the pre-merge head of this branch. The 14 errors areImportError: cannot import name 'load_receipt' from 'scripts.replay_capture'intests/test_capture_cold_restart.py— proven pre-existing on purealphaitself (pytest tests/test_capture_cold_restart.pyon a72062663worktree → the same 14 errors); both files arrive from alpha and are untouched by this merge or the fix.What is not verified
/vol/packet-cachebehavior on a real Modal volume (permissions, cross-process visibility) is covered via theHSS_PACKET_CACHEoverride, not a live volume; purge failure modes use a monkeypatchedrmtree.scripts/check_repo_hygiene.pycurrently fails onbackend/server/orchestrator/tests/data/packets/synthetic-meter.tar: deny-list— pre-existing onalpha(verified on a72062663worktree), not caused by this PR; it needs a separate alpha-side fix.Assumptions
capture.deletedlog and the purge-warning semantics are unchanged, and no test pinned the old purge→tombstone order.UPLOAD_TOKENS_ALLOW_MISSING_SECRETfrom3728d1b1stands; this burst did not reopen it.🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact