Repository navigation
Description
id(web): PR #1696's upload-name uniquifier is check-then-act — concurrent same-name uploads still silently un-name the first
Finding
PR #1696 (merged) fixed #1549 for the sequential case: upload_handler resolves a collision-free stored name with uniquify_upload_name (pkgs/id/src/web/routes.rs:1473 on current main 5a34fa70) and only afterwards sets the blob tag under that name (routes.rs:1616). The check and the claim are two separate awaited store operations with nothing serialising them — no lock in the handler or in AppState, and iroh-blobs tags().set is an unconditional write, not a create-if-absent.
Two uploads of the same filename with different bytes that are in flight at the same time can therefore interleave as: both call upload_name_owner (routes.rs:1455) and see the intended name free → both are handed the intended name → both tags().set it, the second overwriting the first. The first image loses its name exactly as in #1549 (its response even reported that name as its stored name and URL), and the filename-keyed metadata below the tag write is then set-if-absent under the same contested name, so the surviving name's metadata can describe the losing upload. The PR's tests exercise strictly sequential uploads, so the interleaving is untested.
Evidence
- PR: fix(id): uniquify colliding upload stored names #1696 (merged, commit
4d423fbd) — "fix(id): uniquify colliding upload stored names" for id(web): image upload stores by filename with no collision handling — a second paste named 'image.png' silently un-names the first #1549 (closed). - Current main
5a34fa707001f297fd963ea7b8a33d0eb8b7951c,pkgs/id/src/web/routes.rs:upload_name_ownerat :1455 (a plaintags().get),uniquify_upload_nameat :1473-1500 (get-only loop, returns a name it has not claimed), the handler calls it at :1602 and performs the claimingstate.store.tags().set(tag, hash)at :1616 — a separate awaited call with no lock, retry, or post-set verification between them. AppState(pkgs/id/src/web/mod.rs:139-154) holds the blobstoreshared by all requests and contains no upload/name mutex; axum servesupload_handlerconcurrently per connection.- The uniquifier's own doc comment promises "a later upload never silently un-names an earlier, different image" (
routes.rs:1470-1472) — under concurrency that guarantee does not hold. - Deduped against open + closed issues: id(web): image upload stores by filename with no collision handling — a second paste named 'image.png' silently un-names the first #1549 (closed) is the sequential origin and its fix is this PR; id(fileops): archive tags collide within one second — a second replace of the same name silently overwrites the first replace's archive #1612 (closed) is the analogous archive-tag collision in fileops, a different store path. No issue covers the concurrent-upload interleaving.
Why it matters
Pasting images is the upload path's normal use, and two pastes/uploads racing (two tabs, two collaborators on a shared node, a retry overlapping the original) is ordinary, not adversarial. When it happens, the failure is the precise silent data loss #1549 was filed to eliminate — one image quietly un-named while its uploader was told the name was theirs — just narrowed to the concurrent window the fix left open.
Acceptance criteria
- Name resolution and tag claiming are atomic in effect: two concurrent uploads of the same filename with different bytes both end up listed and resolvable under distinct stored names (e.g. the resolve+set sequence is serialised, or the claim is retried/re-resolved when the tag changed underneath), and each response reports the name that actually names its blob.
- A regression test drives two same-name different-bytes uploads concurrently (interleaved at the resolve/set boundary) and pins that neither un-names the other; the existing sequential id(web): image upload stores by filename with no collision handling — a second paste named 'image.png' silently un-names the first #1549 tests stay green, including the idempotent identical-bytes re-upload.
- Filename-keyed metadata for a raced name cannot mix the two uploads (the surviving name's created/content-type records describe the blob the name resolves to).
Agent: pr-diff-review-b
State: filed
Source PR: #1696
Activity
developing-today commentedon Oct 29, 2026
CODE-1201
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
5a34fa70by code reading:uniquify_upload_name(pkgs/id/src/web/routes.rs:1473) only reads tag ownership viaupload_name_owner(:1455); the claimingtags().sethappens separately inupload_handlerat :1616, with no lock in the handler orAppStateand no create-if-absent semantics, so two concurrent same-name uploads can both resolve the intended name and the second set silently un-names the first.9 remaining items
dezren39 commentedon Nov 1, 2026
agent-note:v1 — queue task 5250 (lane C) — picked up for implementation (resume)
Branch
queue/5250-2363-upload-name-atomicalready exists at main0f4731ea0with no fix commits yet — resuming from stage branch_created. Plan: make upload-name resolve+claim atomic inpkgs/id/src/web/routes.rs(AppState upload-name mutex across uniquify+set, or claim-and-verify), add the interleaved concurrent same-name regression test, keep sequential #1549 semantics unchanged.fix(id/web): make upload-name claiming atomic (#2363)
Merge pull request #3425 from developing-today/queue/5250-2363-upload…
dezren39 commentedon Nov 1, 2026
agent-note:v1 — queue task 5250 (lane C) — done: fixed and merged
PR #3425 merged as
7d0c1a82595d507db3181d67d1df99d273b3c5a4(head23299301d).AppState.upload_name_lock(Arc<Mutex<()>>) held inupload_handleracross uniquify-resolve, thetags().setclaim, and all filename-keyed metadata writes — resolve+claim is now atomic for handlers sharing one AppState. Sequential id(web): image upload stores by filename with no collision handling — a second paste named 'image.png' silently un-names the first #1549 semantics unchanged.concurrent_uploads_keep_names_and_metadata_consistentinterleaves two same-name different-bytes uploads at the resolve/set boundary and asserts distinct stored names, per-blob responses, listing/resolvability, and metadata consistency.cargo test --features web --lib upload_router_tests— 13 passed, 0 failed (first attempt failed only on the fresh worktree missing the gitignoredweb/distRustEmbed folder; copied from the primary clone and rerun green). Codex review on23299301d: completed, no findings.