Cover a curl upload sharing its handle with another coroutine - #292
Merged
Merged
Conversation
The IO event of a descriptor is shared by every operation on it, so the read curl parks on is notified by writes it never submitted, and curl_async_read_complete took whatever request the notification carried: curl copied that request's buffer into its upload buffer and freed it, while its owner freed it again. The fix is in php-src 15dff751a9f. The test uploads from a handle another coroutine writes to, and asserts the method the responder echoes: what reaches the server is whatever the two coroutines left in the file, while the crash is what the test is after.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Both coroutines share one descriptor offset, so the writer consumes bytes the upload would have sent: with the body announced as the whole file, curl reached EOF short of the length it promised, and the test rested on how libcurl answers a short upload. The body is announced as 200000 of 1050000 now, which the writer cannot exhaust. Also names what the changelog entry claims and what it derives: the foreign bytes in the POST body follow from the code and were not observed on the wire.
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #291.
curl_async_readsubmitsZEND_ASYNC_IO_READ, returnsCURL_READFUNC_PAUSEand parks onio->event— the event shared by every operation on that descriptor.curl_async_read_completetook whatever request the notification carried as its own: the nextcurl_async_readcopied that request's buffer into curl's upload buffer and calledreq->dispose(req), whilephp_stdiop_writefreed the same request as its owner.A file handle passed as
CURLOPT_READDATAwhile another coroutine writes to it ends the process withzend_mm_heap corrupted (double free). A gdb trace onlibuv_io_req_disposeshows one address freed byphp_stdiop_write(main/streams/plain_wrapper.c:637), then five times bycurl_async_read(ext/curl/curl_async.c), then byphp_stdiop_writeagain, where the allocator panics:The upload also carries the foreign write's bytes rather than the file's, because
req->bufof a write request is the source of that write. That one follows from the code and was not observed on the wire.The engine side is php-src
8f25e009bef(true-async, merged intotrue-async-stable), two commits:curl_async_readsubmitted, and a completion for any other request returns without touching the state;curl_async_readtook for the end of the file and turned into a silently truncated body. A cancelled state also drops its subscription before freeing itself, so a later notification on that descriptor does not read through it.Of the php-src consumers of this event only curl went unfiltered:
php_stdiop_readandphp_stdiop_writehave their ownwhile (!req->completed)loop. This is #130 seen from the other end.This PR carries the test and the changelog entry.
tests/curl/070-read_takes_only_its_completion.phpt: SIGSEGV in 5 runs of 5 against the engine before the change, 10 green runs of 10 after. Both coroutines share one descriptor offset, so the writer consumes bytes the upload would have sent: the body is announced as 200000 of the file's 1050000, which the writer's 819200 cannot exhaust, and the test does not rest on how libcurl answers a short upload. Run here through a copy with an absolute include, because a test including../../../../ext/curl/tests/server.incleaves the checkout through the local symlink — the same reason063and064are red on this machine and green on CI.ext/curl/tests,tests/curlandtests/io: 370 tests, 8 failures. Four are the symlink artefacts (063,064,070,io/082); the other four —curl_curlfile_seek,curl_readfunction_throws_abort,curl_writefunction_throws_abort,curl_headerfunction_throws_abort— are red on this machine with the change and without it alike.