Three curl defects the php-src suite was already reporting - #36
Merged
Merged
Conversation
added 4 commits
September 10, 2026 13:54
A CURLFile part was registered with no seek callback, so libcurl could not replay the body it had already read. A redirect or an authentication challenge then ended the transfer with CURLE_ABORTED_BY_CALLBACK instead of resending the file: ext/curl/tests/curl_curlfile_seek.phpt printed nothing where it expects the part's name, type and length. The callback seeks the stream the read callback opened, and answers CURL_SEEKFUNC_CANTSEEK where the part cannot be replayed from that position: with a read in flight, whose completion would land at the position being left. A read that already finished holds bytes from there and is released, and the flags a finished or failed read left behind are cleared, so the replay starts clean. Upstream carries this callback and the fork lost it when the CURLFile stream was made lazy: opening on first read left nothing for a seek to act on at registration time, and the argument was passed as NULL.
A user callback that throws ends the transfer, and curl_errno() said nothing about it: an exception from CURLOPT_WRITEFUNCTION or CURLOPT_HEADERFUNCTION left the handle reporting CURLE_OK, and every other callback reported CURLE_ABORTED_BY_CALLBACK whatever it was. Upstream has no such gap, because the callback returns to libcurl and libcurl decides; here the exception is stored and the transfer is torn down beside libcurl, so the code has to be carried. The code the callback stands for is stored with the exception -- CURLE_WRITE_ERROR for a write or header callback, CURLE_SEND_FAIL_REWIND for a seek one, CURLE_ABORTED_BY_CALLBACK for the rest -- and the first callback to throw decides it. Every completion path reports it in place of the code libcurl reached on its own. It is stored twice, on the event and on the handle, because the two readers live at different times: the completion paths read the event while it is alive, and curl_async_perform() reads after the suspend the exception ends, by which point the notification may already have disposed the event. Reading it from the event there was a use-after-free waiting for a test to notice. Evidence: ext/curl/tests/curl_writefunction_throws_abort.phpt and curl_headerfunction_throws_abort.phpt, which assert CURLE_WRITE_ERROR and were red.
CURLOPT_READFUNCTION set back to null with no CURLOPT_INFILE behind it leaves the read dispatch with no descriptor, and it answered CURL_READFUNC_ABORT: the next curl_exec() on that handle failed with CURLE_ABORTED_BY_CALLBACK where it had nothing to send. Upstream reads through read_handler->fp and returns 0 when there is none, which is an empty body. An abort stays the answer where a source was named and turned out unusable. Evidence: the second half of ext/curl/tests/curl_readfunction_throws_abort.phpt, which asserts CURLE_OK there.
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.
ext/curl/testshad four failures on this tree, unrelated to any async issue on thetracker and failing the same way on a build of
true-asyncwithout any of my recentcommits. Three defects behind them, one commit each.
A CURLFile part could not be rewound
curl_mime_data_cb()was givenNULLwhere libcurl expects a seek callback, so apart whose body had already been read could not be replayed. A redirect ended the
transfer with
CURLE_ABORTED_BY_CALLBACK, andcurl_curlfile_seek.phptprintednothing where it expects
curl_testdata1.txt|application/octet-stream|6.Upstream carries this callback. The fork lost it when the CURLFile stream was made
lazy: with nothing open at registration time there was nothing for a seek to act
on, and the argument was passed as
NULL. The new one seeks the stream the readcallback opened, and answers
CURL_SEEKFUNC_CANTSEEKwhere the part cannot bereplayed — with a read in flight, whose completion would land at the position being
left. A finished read holds bytes from the old position and is released; the flags a
finished or failed read left are cleared.
curl_errno() said nothing about a callback that threw
An exception from
CURLOPT_WRITEFUNCTIONorCURLOPT_HEADERFUNCTIONleft thehandle reporting
CURLE_OK, and every other callback reportedCURLE_ABORTED_BY_CALLBACKwhatever it was.curl_writefunction_throws_abort.phptandcurl_headerfunction_throws_abort.phptassert
CURLE_WRITE_ERRORand were red.Upstream has no such gap: the callback returns to libcurl and libcurl decides. Here
the exception is stored and the transfer is torn down beside libcurl, so the code
has to be carried. It is now stored with the exception —
CURLE_WRITE_ERRORfor awrite or header callback,
CURLE_SEND_FAIL_REWINDfor a seek one,CURLE_ABORTED_BY_CALLBACKfor the rest — and the first callback to throw decides.It is stored twice, on the event and on the handle, because the two readers live at
different times: the completion paths read the event while it is alive, and
curl_async_perform()reads after the suspend the exception ends, by which pointthe notification may already have disposed the event. An earlier draft of this
change read it from the event there and it was a use-after-free; the two-store shape
is what the test caught.
An empty upload aborted the next transfer
CURLOPT_READFUNCTIONset back tonullwith noCURLOPT_INFILEbehind it leavesthe read dispatch with no descriptor, and it answered
CURL_READFUNC_ABORT. Thesecond
curl_exec()incurl_readfunction_throws_abort.phpttherefore failed whereit had nothing to send. Upstream returns 0 in that case, which is an empty body. An
abort stays the answer where a source was named and turned out unusable.
Checks
ext/curl/tests+ext/async/testsext/curl/tests+ext/async/tests/curlconfig.m4admits)ext/async/fuzzy-tests/_generated— 724 passed, 0 failed. Before these commitsext/curl/testswas 168 passed, 4 failed on 8.12.1 and 164 passed, 4 failed on 8.5.0.