Skip to content

refactor(http): harden multipart parsing and upload lifecycle - #6309

Merged
matyhtf merged 19 commits into
masterfrom
refactor/6161-6162-upload
Oct 16, 2026
Merged

matyhtf merged 19 commits into
masterfrom
refactor/6161-6162-upload

Conversation

@matyhtf

@matyhtf matyhtf commented Oct 16, 2026

Copy link
Copy Markdown
Member

Summary

Integrate and refine the multipart upload changes from #6161, #6162, and #6154 for master.

  • Parse fragmented multipart headers consistently across preprocessing, PHP request parsing, HTTP/2, and the C++ HTTP wrapper, with bounded header accumulation.
  • Track preprocessed upload provenance and temporary-file ownership independently of PHP metadata parsing; clean up files after rejected metadata, interrupted requests, and dispatch failures.
  • Reject client-supplied reserved upload headers and duplicate Content-Disposition headers, and handle buffered file write/flush failures.
  • Keep upload paths and parsing buffers in multipart-specific state to avoid extra allocation for ordinary HTTP requests.
  • Reject incomplete HTTP/2 multipart streams while keeping the connection available for subsequent requests.
  • Use PHP's platform temporary directory for manually created requests and replace timing-dependent thread status assertions with queue synchronization.

This is a substantial refactor intended for master; it should not be backported wholesale to 6.1 or 6.2.

Validation

  • Focused multipart/upload PHPT suite: 21 passed.
  • Focused C++ suite: 22 passed.
  • Valgrind checks for upload ownership, parser cleanup, and header limits passed.
  • Follow-up Windows PHP 8.4 ZTS and Linux regression suites: 5 passed on each platform.
  • Latest branch CI passed: https://github.com/swoole/swoole-src/actions/runs/38884852408

An earlier broader local HTTP/2 run also exposed a session_shutdown.phpt debug assertion, reproduced with the pre-fix extension; this was independent of these changes.

Merge strategy

Squash merge into master.

binaryfire and others added 19 commits October 16, 2026 15:11
The large-upload preprocessor rebuilds multipart bodies and adds Swoole-Upload-File after saving a file part. A client-provided copy was previously written into that rebuilt body and later treated as generated upload metadata.

Reject the reserved header while parsing client input and return a bad request. Log the specific cause through the existing invalid-request error code so operators can suppress repeated client errors.

Rejection after an earlier file part follows the parser's existing abort cleanup and can leave that earlier temporary path behind. That needs per-request path ownership and remains separate from this focused fix. The callback warning provides the useful reason; the existing generic parser notice may remain empty rather than adding a new multipart parser error code here.

(cherry picked from commit 8cf1af1)
Force the large-upload preprocessor and send the reserved header both in the first file part and after a completed file part. Both requests must return a bad request without reaching the application handler or registering the supplied paths as uploaded files.

Suppress partial-write warnings because an early rejection may close the connection before the client finishes sending. Use an isolated upload directory and remove existing abort-path files after the assertions without making that separate cleanup behavior part of this test.

(cherry picked from commit de11e2d)
Keep generated upload paths owned by the reactor until request dispatch succeeds. Remove them when parsing, size validation, or dispatch fails.

Return a service-unavailable response when temporary-file setup fails, and reject malformed multipart boundaries before the parser can be dereferenced.

(cherry picked from commit 1224813)
Cover malformed boundaries, reserved-header orderings, temporary-file setup failure, rebuilt-body limits, worker survival, and cleanup of every generated upload path.

Use the logged rebuilt content length to prove the size-limit case passed through the multipart preprocessor.

(cherry picked from commit 1850907)
BASE mode may run the request handler inside dispatch_request() and destroy the reactor-side Request before the call returns. Moving its temporary-path list afterward therefore accessed freed memory.

Move the path list before dispatch, remove the generated files when dispatch fails, and cover successful preprocessed upload dispatch and cleanup in BASE mode.

(cherry picked from commit 74e4302)
Do not create an upload temporary file or internal marker for filename="" while preprocessing a request containing another large file. Leave the empty part for the PHP multipart consumer so it retains UPLOAD_ERR_NO_FILE.

Also drop untested fdopen failure handling that was added earlier to this pull request without a reproducible path.

(cherry picked from commit e68ceb1)
Force the large-upload preprocessing path with one real file and verify that a second empty file input keeps its normal error code and empty temporary path.

(cherry picked from commit 4fe52f6)
Remove per-PR package.xml entries for the added PHPTs.

Swoole updates the packaged test list in release commits rather than in individual bug fixes.

(cherry picked from commit e349638)
Carry the preprocessed-request state through server dispatch and only interpret internal upload markers on that path. Reject and unlink unmatched generated paths, and release temporary parser state on malformed metadata exits.

(cherry picked from commit 962c8b2)
Cover ordinary and coroutine request paths, valid preprocessed uploads, malformed generated markers, temporary-file cleanup, and worker survival.

(cherry picked from commit 1341a90)
Key wrapper uploads by their multipart field name and reject generated markers without matching file metadata. Remove unmatched and duplicate generated paths without stopping multipart parsing, so later upload files remain visible for cleanup.

(cherry picked from commit 05705ea)
Assert direct and preprocessed wrapper uploads use the expected field key. Cover nameless and duplicate generated paths, including continued parsing and temporary-file cleanup in the C++ and PHP paths.

(cherry picked from commit 9e37f2d)
Buffer fragmented multipart header names and values until each header is complete, propagate parser callback errors, and reject incomplete bodies once their declared length has been consumed.

Reset derived state for repeated Content-Type fields, close temporary upload handles before cleanup, and allow large-upload preprocessing on Windows.

(cherry picked from commit 2f58ba2)
Cover fragmented header fields and values, parser callback failures, repeated Content-Type replacement, incomplete bodies, valid split closing boundaries, and oversized upload preprocessing.

(cherry picked from commit ca0e3f1)
Exercise split untrusted upload markers in manual parsing and preprocessing. Verify fragmented uploads and truncated-body cleanup in BASE and PROCESS modes, and adapt the rebuilt-body limit assertion to earlier validation.
Move temporary upload path tracking from Request to FormData to avoid increasing ordinary HTTP request size. Retain multipart state through dispatch and preserve cleanup on failure.
Collect preprocessed upload paths independently of PHP metadata parsing and clean up rejected or interrupted uploads. Reject duplicate dispositions, check buffered file errors, and share bounded multipart header handling.

Keep multipart state lazy, reject incomplete HTTP/2 multipart streams, and add regression coverage for cleanup, dispatch failures, header limits, and file descriptor leaks.
Use the platform temporary directory instead of hardcoding /tmp in Request::create(). Cover default and explicit upload directories and unfinished upload cleanup on Windows.
Replace short sleeps and random worker lifetimes with queue handshakes. Keep workers alive during status assertions and bound the detached thread exit wait.
@matyhtf
matyhtf merged commit 0306f53 into master Oct 16, 2026
19 checks passed
@matyhtf
matyhtf deleted the refactor/6161-6162-upload branch October 16, 2026 11:38
@codecov

codecov Bot commented Oct 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.78571% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.81%. Comparing base (5a85b60) to head (e1f83dc).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/protocol/http.cc 73.17% 33 Missing ⚠️
src/wrapper/http.cc 82.27% 14 Missing ⚠️
src/server/port.cc 63.63% 4 Missing ⚠️
thirdparty/multipart_parser.c 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6309      +/-   ##
==========================================
+ Coverage   83.72%   83.81%   +0.08%     
==========================================
  Files         114      114              
  Lines       18771    18939     +168     
  Branches     3347     3381      +34     
==========================================
+ Hits        15716    15873     +157     
- Misses       3055     3066      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ChiragAgg5k added a commit to ChiragAgg5k/appwrite that referenced this pull request Oct 23, 2026
ChiragAgg5k added a commit to appwrite/appwrite that referenced this pull request Oct 24, 2026
…pter

Swoole's multipart parser honors an internal `Swoole-Upload-File` part
header and pins part metadata to a client-chosen `tmp_name` with
`error: 0` and the real size of the target file
(swoole/swoole-src#6309). Because `is_uploaded_file()` trusts Swoole
metadata, a remote unauthenticated request can make upload-handling code
read an arbitrary server-local file (e.g. /etc/passwd) as if it were a
standard multipart upload. No released Swoole version contains the
upstream fix yet (`upload_preprocessed` lands with 6.3/master).

The Swoole adapter is the single choke point: genuine uploads are always
written to `<upload_tmp_dir>/swoole.upfile.XXXXXX`, so getFiles() now
rejects any `tmp_name` outside those directories/that naming scheme,
neutralizing spoofed entries into UPLOAD_ERR_CANT_WRITE with no size,
while real uploads (flat or bracketed list form) pass through untouched.

Parser-level regression tests cover the spoofed marker through
SwooleRequest::parse(), genuine uploads, mixed lists, wrong-dir and
nonexistent paths.
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.

2 participants