HexBrain

curl · Report #3824303

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

Informativestatus
lowseverity
19upvotes
28 Jun 2026disclosed

How it was found

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():

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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original