curl · Report #4123749
WebSocket decoder use-after-free when curl_easy_pause() unpauses from a debug callback
How it was found
Summary
At lib/ws.c:503, ws_dec_pass_payload() holds a pointer into the WebSocket receive queue while passing a frame to the application's write callback. If that callback returns CURL_WRITEFUNC_PAUSE, the PAUSE requested trace invokes the debug callback before the WebSocket pass consumes those bytes (CURLOPT_VERBOSE=1 and curl_global_trace("write") enabled). Calling curl_easy_pause(CURLPAUSE_RECV) then curl_easy_pause(CURLPAUSE_CONT) there starts nested ws_cw_flush(). It frees the queued chunk before the outer pass reads it; in my curl 8.23.0 run, the PoC's client exits 0 after receiving all 200,005 bytes, but byte offset 32,756 differs from the server's stream, and ASan reports a 65,518-byte use-after-free read.
Affected version
- curl 8.22.0 and 8.23.0 on Linux x86-64: reproduced;
b940cb1566introduced this path in 8.22.0; attached ASan trace is from 8.22.0 - Master at
629646d(17 October 2026): same unguarded path confirmed by source inspection
This reports a different WebSocket queue from report #4102061: applying the proposed cw-out and cw-pause guards from the still-open PR #23431 leaves this PoC corrupting output; the attached diff adds regression test 2312.
I used AI assistance to find and patch this; I reproduced the Release corruption and ASan UAF with the attached PoC.
Steps To Reproduce
-
Check out
curl-8_23_0and build static libcurl with WebSockets and verbose strings:cmake -S . -B bld-vuln -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DBUILD_SHARED_LIBS=OFF -DCURL_USE_OPENSSL=ON -DENABLE_DEBUG=OFF -DCURL_DISABLE_VERBOSE_STRINGS=OFF cmake --build bld-vuln --target libcurl_static -j2 -
Put the attached
client.c,server.py, andrun.shin one directory; runsh run.sh /absolute/path/to/curl/bld-vuln vulnerablethere to see the changed output -
Build the same tag with ASan and run
sh run.sh /absolute/path/to/curl/bld-asan asan:cmake -S . -B bld-asan -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF -DBUILD_SHARED_LIBS=OFF -DCURL_USE_OPENSSL=ON -DENABLE_DEBUG=OFF -DCURL_DISABLE_VERBOSE_STRINGS=OFF -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" cmake --build bld-asan --target libcurl_static -j2 -
Apply
ws-fix-and-test.diff, configure and build with the Release flags from step 1 usingbld-fixedinstead ofbld-vuln, then runsh run.sh /absolute/path/to/curl/bld-fixed fixedfor the clean control
Impact
A WebSocket server can supply the large frame, but the client must use the callback pattern above; libcurl can then deliver bytes from reused heap storage to that application's write callback, with no peer-visible leak or code execution demonstrated.