UAF read in mev_pollset_diff() trace path after curl_easy_pause() in socket callback

Disclosed: 2026-06-28 15:22:06 By homanp To curl
Low
Vulnerability Details
## Summary: The CVE-2026-9080 fix re-fetches the sh_entry after the socket callback inside `mev_sh_entry_update()`, because `curl_easy_pause()` called from that callback re-enters `mev_assess()` and can free the entry. The same re-fetch was not applied at the caller, `mev_pollset_diff()`, which dereferences its own `entry` pointer again in a `CURL_TRC_M()` trace call immediately after the callback returns (`lib/multi_ev.c:435-436`). On the exact reentrancy and free path that 9080 patched, that trace reads `&entry->xfers` and `entry->conn` from freed memory. It only fires in verbose-strings builds (the default) with multi tracing enabled, because the dereference lives inside the trace macro's arguments, which is why the 9080 PoC did not surface it. This is an incomplete-fix residual of 9080 rather than a new issue, with a one-line fix. ## Affected version Reproduced on libcurl 8.21.0 / current git master (the release that shipped the 9080 fix). ## Steps To Reproduce: 1. Build libcurl with verbose strings (default) and AddressSanitizer. 2. Use the `curl_multi_socket_action()` event interface with a registered `CURLMOPT_SOCKETFUNCTION`. 3. From inside the socket callback, call `curl_easy_pause(easy, CURLPAUSE_RECV)` on a transfer whose socket action is being removed (the same trigger as CVE-2026-9080), so the reentrant `mev_assess()` frees the sh_entry for that socket. 4. Enable multi tracing (`--trace-config all`, or a handle where `CURL_TRC_M_is_verbose()` is true). 5. ASan reports a heap-use-after-free read in `mev_pollset_diff()` at the `CURL_TRC_M()` call right after `mev_sh_entry_update()` (`lib/multi_ev.c:435-436`): `Curl_uint32_spbset_count(&entry->xfers)` and `entry->conn` read the freed entry. ## Suggested fix Re-fetch and guard after the callback, matching the idiom already used in `mev_sh_entry_update()`: ```c mresult = mev_sh_entry_update(multi, data, entry, s, prev_ps->actions[i], 0); if(mresult) return mresult; /* the callback may have freed this entry via curl_easy_pause(); re-fetch */ entry = mev_sh_entry_get(&multi->ev.sh_entries, s); if(entry) CURL_TRC_M(data, "ev entry fd=%" FMT_SOCKET_T ", removed transfer, " "total=%u/%d (xfer/conn)", s, Curl_uint32_spbset_count(&entry->xfers), entry->conn ? 1 : 0); ``` Alternatively capture the two counts into locals before the call and trace those, or drop the entry-derived fields from the trace line. ## Impact ## Summary: A use-after-free read of a freed `mev_sh_entry`: in practice a crash (DoS), or stale heap bytes read into the application's multi trace output. It is a read, not a write, so there is no write primitive and no control-flow hijack. The stale bytes only reach the application's own trace/debug sink, so by themselves they are not disclosed to a remote party. Triggering requires the multi socket event interface, a socket callback that pauses so the entry is freed, and multi tracing enabled in a verbose-strings build; as the team noted for 9080, pausing from a socket callback is unusual, so exposure is narrow. I would rate this Low, strictly below 9080 (rated Low) since it adds the tracing condition. Proposed vector CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L, deferring to your scoring.
Actions
View on HackerOne
Report Stats
  • Report ID: 3824303
  • State: Closed
  • Substate: informative
  • Upvotes: 2
Share this report