HexBrain

curl · Report #4124722

WebSocket send queue use-after-free during nested curl_ws_send()

Informativestatus
lowseverity
2upvotes
20 Oct 2026disclosed

How it was found

At lib/ws.c:1734, ws_flush() borrows a pointer from the outgoing sendbuf and passes it to ws_send_raw_blocking(); it skips those bytes only after the send returns. A curl_ws_send() from the WebSocket write callback can be interrupted by a TCP-trace debug callback that calls curl_ws_send() again, and the nested flush frees the chunk the outer send is using. On curl-8_23_0 with ENABLE_DEBUG=OFF, ASan reports a 32,767-byte use-after-free read in send(), while the no-nested control is clean. In a Release run, the first decoded frame differs at byte 32,754, a duplicate frame header appears inside it at raw offset 32,768, and 32,767 bytes from a test allocation that reused the freed chunk reach the server in the raw tail. This path does not use curl_easy_pause() and survives the proposed fixes for my earlier reports.

Steps to reproduce

  1. ASan build, nested trigger, and no-nested control:

    POC=/absolute/path/to/h1-attachments
    git checkout curl-8_23_0
    cmake -S . -B bld-asan -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DENABLE_DEBUG=OFF -DCURL_DISABLE_VERBOSE_STRINGS=OFF -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g"
    cmake --build bld-asan --target libcurl_static -j2
    sh "$POC/run.sh" "$PWD/bld-asan" nested   # UAF, READ of size 32767
    sh "$POC/run.sh" "$PWD/bld-asan" control  # CLEAN
    
  2. Release build, canary, and control:

    cmake -S . -B bld-release -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DENABLE_DEBUG=OFF -DCURL_DISABLE_VERBOSE_STRINGS=OFF
    cmake --build bld-release --target libcurl_static -j2
    sh "$POC/run.sh" "$PWD/bld-release" canary   # CORRUPT, 32767-byte 0xA5 run
    sh "$POC/run.sh" "$PWD/bld-release" control  # CLEAN
    
  3. Fixed control and regression test:

    git apply "$POC/ws-send-fix-and-test.diff"
    cmake --build bld-asan --target libcurl_static -j2
    sh "$POC/run.sh" "$PWD/bld-asan" nested   # CLEAN
    

    Test 2313 in the diff fails before the fix and passes after it.

Source history points to commit 02edae54e8, first included in 8.12.0; the earliest release where I reproduced the UAF is 8.15.0, while 8.10.0 was clean. I did not run 8.12.0.

Impact

A WebSocket peer can receive bytes from reused heap memory when an application uses this nested callback pattern

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