Hi, there is a potential bug in the way the CurlHolder RAII wrapper is implemented.
This bug was reproduced on 53e2d28.
Description
What crashes and where:
- The failure is an AddressSanitizer double-free in libcurl during curl_easy_cleanup, called from cpr::CurlHolder::~CurlHolder(). The stack exactly matches the CASR report: free -> libcurl -> curl_easy_cleanup -> cpr::CurlHolder::~CurlHolder.
Root cause:
- cpr::CurlHolder is an owning RAII wrapper for a CURL easy handle and related resources (CURL*, curl_slist*, curl_mime*). In the header, CurlHolder declares:
- move assignment operator = default;
- copy assignment operator = default; (and copy/move ctors as default)
- Defaulted move assignment for a type with raw owning pointers performs a member-wise move that is equivalent to copying the pointer values. The source object is not nulled; both destination and source now point to the same underlying CURL handle and lists. When both objects are destroyed, curl_easy_cleanup is invoked twice on the same handle, leading to a double-free inside libcurl. The provided minimal and the original fuzzer-derived testcase both deterministically reproduce this.
Suggested fix:
- Make CurlHolder non-copyable and implement a custom move constructor and move-assignment operator that transfer ownership and null the source:
- CurlHolder(const CurlHolder&) = delete; CurlHolder& operator=(const CurlHolder&) = delete;
- CurlHolder(CurlHolder&&) noexcept; CurlHolder& operator=(CurlHolder&&) noexcept; in both, set source.handle = nullptr; source.chunk = nullptr; source.resolveCurlList = nullptr; source.multipart = nullptr; and move the error buffer safely.
- Alternatively, wrap all raw resources into unique_ptr-like wrappers with custom deleters so defaulted moves become safe.
- Audit other defaulted special members for similar issues (copy ctor/assignment are currently defaulted and will also lead to double-free if used).
POC
The following testcase demonstrates the bug:
testcase.cpp
#include "/fuzz/install/include/cpr/curlholder.h"
#include <utility>
int main(){
cpr::CurlHolder a;
cpr::CurlHolder b;
// Defaulted move-assignment leaves both objects with identical raw pointers.
a = std::move(b);
// End of scope: both destruct -> two curl_easy_cleanup() calls on the same CURL* -> double-free.
return 0;
}
stdout
stderr
=================================================================
==1==ERROR: AddressSanitizer: attempting double-free on 0x5020000067b0 in thread T0:
#0 0x55e1dc4034d6 in free (/fuzz/test+0xc84d6) (BuildId: 3491d7863e54733a14f847228b77c5ad0212a302)
#1 0x7f07009fddfe (/lib/x86_64-linux-gnu/libcurl.so.4+0x82dfe) (BuildId: 59bfa6e2bed3cd9020aa262e6af261535d76ee40)
#2 0x7f070099982b in curl_easy_cleanup (/lib/x86_64-linux-gnu/libcurl.so.4+0x1e82b) (BuildId: 59bfa6e2bed3cd9020aa262e6af261535d76ee40)
#3 0x55e1dc4429cf in cpr::CurlHolder::~CurlHolder() (/fuzz/test+0x1079cf) (BuildId: 3491d7863e54733a14f847228b77c5ad0212a302)
#4 0x55e1dc442710 in main /fuzz/testcase.cpp:10:1
#5 0x7f06fff2ed8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#6 0x7f06fff2ee3f in __libc_start_main csu/../csu/libc-start.c:392:3
#7 0x55e1dc3673d4 in _start (/fuzz/test+0x2c3d4) (BuildId: 3491d7863e54733a14f847228b77c5ad0212a302)
0x5020000067b0 is located 0 bytes inside of 16-byte region [0x5020000067b0,0x5020000067c0)
freed by thread T0 here:
#0 0x55e1dc4034d6 in free (/fuzz/test+0xc84d6) (BuildId: 3491d7863e54733a14f847228b77c5ad0212a302)
#1 0x7f07009fddfe (/lib/x86_64-linux-gnu/libcurl.so.4+0x82dfe) (BuildId: 59bfa6e2bed3cd9020aa262e6af261535d76ee40)
previously allocated by thread T0 here:
#0 0x55e1dc403968 in calloc (/fuzz/test+0xc8968) (BuildId: 3491d7863e54733a14f847228b77c5ad0212a302)
#1 0x7f07009d9d6c (/lib/x86_64-linux-gnu/libcurl.so.4+0x5ed6c) (BuildId: 59bfa6e2bed3cd9020aa262e6af261535d76ee40)
SUMMARY: AddressSanitizer: double-free (/fuzz/test+0xc84d6) (BuildId: 3491d7863e54733a14f847228b77c5ad0212a302) in free
==1==ABORTING
Steps to Reproduce
The crash was triaged with the following Dockerfile:
Dockerfile
# Ubuntu 22.04 with some packages pre-installed
FROM hgarrereyn/stitch_repro_base@sha256:3ae94cdb7bf2660f4941dc523fe48cd2555049f6fb7d17577f5efd32a40fdd2c
RUN git clone https://github.com/libcpr/cpr /fuzz/src && \
cd /fuzz/src && \
git checkout 53e2d28696e542d4b32b7c42928395494bcafe64 && \
git submodule update --init --remote --recursive
ENV LD_LIBRARY_PATH=/fuzz/install/lib
ENV ASAN_OPTIONS=hard_rss_limit_mb=1024:detect_leaks=0
RUN echo '#!/bin/bash\nexec clang-17 -fsanitize=address -O0 "$@"' > /usr/local/bin/clang_wrapper && \
chmod +x /usr/local/bin/clang_wrapper && \
echo '#!/bin/bash\nexec clang++-17 -fsanitize=address -O0 "$@"' > /usr/local/bin/clang_wrapper++ && \
chmod +x /usr/local/bin/clang_wrapper++
# Install system curl and build tools
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
cmake \
libcurl4-openssl-dev \
ca-certificates \
pkg-config \
ninja-build \
&& rm -rf /var/lib/apt/lists/*
# Configure and build cpr using system curl, static library only
RUN cmake -S /fuzz/src -B /tmp/build -G Ninja \
-DCMAKE_C_COMPILER=clang_wrapper \
-DCMAKE_CXX_COMPILER=clang_wrapper++ \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/fuzz/install \
-DBUILD_SHARED_LIBS=OFF \
-DCPR_USE_SYSTEM_CURL=ON \
-DCPR_BUILD_TESTS=OFF \
&& cmake --build /tmp/build --parallel \
&& cmake --install /tmp/build
Build Command
clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lcpr -lcurl -lssl -lcrypto -lz -lpthread && /fuzz/test
Reproduce
- Copy
Dockerfile and testcase.cpp into a local folder.
- Build the repro image:
docker build . -t repro --platform=linux/amd64
- Compile and run the testcase in the image:
docker run \
-it --rm \
--platform linux/amd64 \
--mount type=bind,source="$(pwd)/testcase.cpp",target=/fuzz/testcase.cpp \
repro \
bash -c "clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lcpr -lcurl -lssl -lcrypto -lz -lpthread && /fuzz/test"
Additional Info
This testcase was discovered by STITCH, an autonomous fuzzing system. All reports are reviewed manually (by a human) before submission.
Hi, there is a potential bug in the way the
CurlHolderRAII wrapper is implemented.This bug was reproduced on 53e2d28.
Description
What crashes and where:
Root cause:
Suggested fix:
POC
The following testcase demonstrates the bug:
testcase.cpp
stdout
stderr
Steps to Reproduce
The crash was triaged with the following Dockerfile:
Dockerfile
Build Command
clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lcpr -lcurl -lssl -lcrypto -lz -lpthread && /fuzz/testReproduce
Dockerfileandtestcase.cppinto a local folder.docker build . -t repro --platform=linux/amd64docker run \ -it --rm \ --platform linux/amd64 \ --mount type=bind,source="$(pwd)/testcase.cpp",target=/fuzz/testcase.cpp \ repro \ bash -c "clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lcpr -lcurl -lssl -lcrypto -lz -lpthread && /fuzz/test"Additional Info
This testcase was discovered by
STITCH, an autonomous fuzzing system. All reports are reviewed manually (by a human) before submission.