HexBrain

curl · Report #4123749

WebSocket decoder use-after-free when curl_easy_pause() unpauses from a debug callback

Informativestatus
8upvotes
19 Oct 2026disclosed

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; b940cb1566 introduced 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

  1. Check out curl-8_23_0 and 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
    
  2. Put the attached client.c, server.py, and run.sh in one directory; run sh run.sh /absolute/path/to/curl/bld-vuln vulnerable there to see the changed output

  3. 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
    
  4. Apply ws-fix-and-test.diff, configure and build with the Release flags from step 1 using bld-fixed instead of bld-vuln, then run sh run.sh /absolute/path/to/curl/bld-fixed fixed for 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.

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