I did this
Calling curl_easy_setopt(easy, CURLOPT_SHARE, NULL) from inside a WRITEFUNCTION/READFUNCTION callback deterministically SEGVs in Curl_xfer_needs_flush on the way back out of the callback. Same setopt called from the main thread between curl_multi_perform iterations leaves the easy stuck in MSTATE_PERFORMING with data->conn == NULL, returning CURLM_INTERNAL_ERROR forever.
I realise calling setopt during a transfer isn't really sanctioned — curl_easy_pause.md is the only function the docs explicitly say is safe inside callbacks, and the rest is implicitly "between transfers". But the failure mode here is a NULL deref rather than a graceful error, and the CURLOPT_SHARE handler takes the un-link path unconditionally so the foot-gun is right at the surface. Also the fix is really simple! So I think maybe we should do it.
The setopt handler at lib/setopt.c:1503-1518 calls Curl_share_easy_unlink with no in-progress guard. Curl_share_easy_unlink (lib/curl_share.c:403-431) lands in Curl_detach_connection (lib/multi.c:938-951), which is the sole site that sets data->conn = NULL. Control returns from the callback into Curl_sendrecv → Curl_req_want_send → Curl_xfer_needs_flush (lib/transfer.c:806-809):
bool Curl_xfer_needs_flush(struct Curl_easy *data)
{
return Curl_conn_needs_flush(data, data->conn->send_idx);
}
data->conn == NULL, NULL deref at offset 0x14c (= offsetof(struct connectdata, send_idx)). The defensive if(!data->conn) return CURLM_INTERNAL_ERROR in multi_runsingle (lib/multi.c:2730-2735) only catches the next runsingle entry — it can't help inside the same runsingle that invoked the callback. The unconditional un-link in CURLOPT_SHARE predates the recent share refactor (82009c4220, 2026-03-09); the shape has been there for a while.
PoC: a write callback that calls curl_easy_setopt(easy, CURLOPT_SHARE, NULL) on its first invocation, against any HTTP server. ASan trace:
==1464774==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000014c
#0 Curl_xfer_needs_flush
#1 Curl_sendrecv
#2 multi_runsingle
#3 main F11_poc.c:259
Simplest fix is to refuse the setopt while the easy is in flight — mirrors share_in_use at lib/curl_share.c:204-208:
case CURLOPT_SHARE: {
struct Curl_share *set = va_arg(param, struct Curl_share *);
+ if(data->multi && data->mstate >= MSTATE_CONNECT &&
+ data->mstate < MSTATE_COMPLETED)
+ return CURLE_BAD_FUNCTION_ARGUMENT;
result = Curl_share_easy_unlink(data);
I expected the following
Error or no crash
curl/libcurl version
master
operating system
all
I did this
Calling
curl_easy_setopt(easy, CURLOPT_SHARE, NULL)from inside aWRITEFUNCTION/READFUNCTIONcallback deterministically SEGVs inCurl_xfer_needs_flushon the way back out of the callback. Same setopt called from the main thread betweencurl_multi_performiterations leaves the easy stuck inMSTATE_PERFORMINGwithdata->conn == NULL, returningCURLM_INTERNAL_ERRORforever.I realise calling setopt during a transfer isn't really sanctioned —
curl_easy_pause.mdis the only function the docs explicitly say is safe inside callbacks, and the rest is implicitly "between transfers". But the failure mode here is a NULL deref rather than a graceful error, and theCURLOPT_SHAREhandler takes the un-link path unconditionally so the foot-gun is right at the surface. Also the fix is really simple! So I think maybe we should do it.The setopt handler at
lib/setopt.c:1503-1518callsCurl_share_easy_unlinkwith no in-progress guard.Curl_share_easy_unlink(lib/curl_share.c:403-431) lands inCurl_detach_connection(lib/multi.c:938-951), which is the sole site that setsdata->conn = NULL. Control returns from the callback intoCurl_sendrecv→Curl_req_want_send→Curl_xfer_needs_flush(lib/transfer.c:806-809):data->conn == NULL, NULL deref at offset 0x14c (=offsetof(struct connectdata, send_idx)). The defensiveif(!data->conn) return CURLM_INTERNAL_ERRORinmulti_runsingle(lib/multi.c:2730-2735) only catches the next runsingle entry — it can't help inside the same runsingle that invoked the callback. The unconditional un-link inCURLOPT_SHAREpredates the recent share refactor (82009c4220, 2026-03-09); the shape has been there for a while.PoC: a write callback that calls
curl_easy_setopt(easy, CURLOPT_SHARE, NULL)on its first invocation, against any HTTP server. ASan trace:Simplest fix is to refuse the setopt while the easy is in flight — mirrors
share_in_useatlib/curl_share.c:204-208:case CURLOPT_SHARE: { struct Curl_share *set = va_arg(param, struct Curl_share *); + if(data->multi && data->mstate >= MSTATE_CONNECT && + data->mstate < MSTATE_COMPLETED) + return CURLE_BAD_FUNCTION_ARGUMENT; result = Curl_share_easy_unlink(data);I expected the following
Error or no crash
curl/libcurl version
master
operating system
all