Skip to content

fix(upload): isolate resumable uploads with a per-request temp path - #3159

Merged
vpetersson merged 3 commits into
masterfrom
worktree-purrfect-exploring-ripple
Jul 8, 2026
Merged

fix(upload): isolate resumable uploads with a per-request temp path#3159
vpetersson merged 3 commits into
masterfrom
worktree-purrfect-exploring-ripple

Conversation

@vpetersson

Copy link
Copy Markdown
Contributor

Issues Fixed

Fixes #3135

Description

The file_asset upload endpoint staged every upload at a deterministic temp path derived from the filename (uuid5(NAMESPACE_URL, filename).hex + '.tmp'), so two uploads of the same name shared one .tmp with no per-upload isolation. That allowed a concurrency race (two chunk requests both opening wb and clobbering each other) and cross-attempt bleed (a stale .tmp from an interrupted upload reused by a later one).

This adds per-upload session isolation:

  • Stage each upload at a random <uuid4>.tmp — never derived from the filename — so concurrent same-name uploads and stale attempts land in separate files.
  • Return an opaque upload_id; a resumable (Content-Range) client echoes it via the X-Upload-Id header so every chunk reassembles into the same file. A malformed id is rejected with 400 (path-traversal guard).
  • Open chunks with os.open(..., O_CREAT) (no O_TRUNC) to close the isfile()-then-open('wb') race.

Orphaned .tmp files are already reaped by the hourly cleanup() Celery task (the random names still match its *.tmp sweep), and the single-POST browser path already used a random uuid4 name, so no client changes are needed for the common case.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

- Stage each file_asset upload at a random <uuid4>.tmp instead of a
  filename-derived one, so concurrent same-name uploads and stale
  attempts can't clobber or bleed into each other
- Return an opaque upload_id the client echoes via X-Upload-Id to
  reassemble a resumable (Content-Range) upload into one file
- Reject a malformed X-Upload-Id (path-traversal guard) with 400
- Open chunks with O_CREAT (no O_TRUNC) to close the check-then-wb race

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from a team as a code owner July 8, 2026 14:05
@vpetersson vpetersson self-assigned this Jul 8, 2026
@vpetersson
vpetersson requested a review from Copilot July 8, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a concurrency and cross-attempt corruption risk in the file_asset upload endpoint by removing the deterministic per-filename temp path and introducing a per-upload session identifier (upload_id) for resumable (Content-Range) uploads.

Changes:

  • Stage uploads to a random <upload_id>.tmp file under assetdir and return upload_id to allow resumable chunk reassembly without filename-derived temp paths.
  • Use os.open(..., O_CREAT) (without truncation) for chunk writes to avoid the isfile()-then-open('wb') clobber race.
  • Add/adjust tests to validate isolation across same-name uploads, truncation behavior on “shrink”, and malformed upload id rejection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/anthias_server/api/views/mixins.py Implements per-upload temp path isolation via upload_id, header-based resumable session binding, and safer file open semantics for chunked uploads.
src/anthias_server/api/tests/test_v1_endpoints.py Updates chunked upload test flow to use upload_id and adds coverage for isolation + malformed id handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/anthias_server/api/views/mixins.py Outdated
Comment thread src/anthias_server/api/tests/test_v1_endpoints.py
Comment thread src/anthias_server/api/tests/test_v1_endpoints.py
Addresses Copilot review on the resumable-upload isolation change.

- Ignore X-Upload-Id on single-shot (non-range) uploads: that path
  uses open('wb'), so an echoed id could truncate another session's
  in-progress .tmp. Without a range, always mint a fresh id.
- Normalize the echoed id's case before the traversal guard.
- Assert response status before reading .data in the upload tests.
- Add a regression test that X-Upload-Id is ignored without a range.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/anthias_server/api/views/mixins.py
Comment thread src/anthias_server/api/views/mixins.py Outdated
Addresses second Copilot review round.

- Open resumable chunks with 0o666 (umask-masked) + O_CLOEXEC so
  permissions and fd-inheritance match the builtin open() used on the
  non-range path, instead of hard-coding 0o644.
- Reword the upload-id comment: the check is a 32-hex-char traversal
  guard, not a UUIDv4 version/variant validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from Copilot July 8, 2026 15:13
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@vpetersson
vpetersson merged commit 952f426 into master Jul 8, 2026
10 checks passed
@vpetersson
vpetersson deleted the worktree-purrfect-exploring-ripple branch July 8, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upload: deterministic per-filename .tmp path is unsafe for concurrent same-name uploads

2 participants