Skip to content

Double-free in ly_realloc when size=0 #2450

@hgarrereyn

Description

@hgarrereyn

Hi, there is a potential bug in ly_realloc reachable by providing size=0.

This bug was reproduced on efe43e3.

Description

ly_realloc is a wrapper around realloc which tries to reallocate, and then frees the original pointer if it failed:

    new_mem = realloc(ptr, size);
    if (!new_mem) {
        free(ptr);
    }

On some platforms/allocators, realloc(ptr, 0) frees ptr and returns NULL (C/POSIX-permitted behavior). In that case, ly_realloc calls free(ptr) again, causing a double free. The provided testcase sets new_size=0, triggering this exact path. AddressSanitizer confirms the first free happens inside realloc and the second in ly_realloc.

Internally, it seems like ly_realloc is used in quite a few places, often with sizes derived from user controlled data. It seems possible that one of those paths could hit it with new_size=0, thus triggering this double free.

Since the double-free happens back-to-back, it seems likely that modern libc assertions would catch this and abort the process, thus impact is likely fairly low.

POC

The following testcase demonstrates the bug:

testcase.cpp

#include <cstdlib>
#include <cstddef>

extern "C" void *ly_realloc(void *ptr, size_t size);

int main() {
    void *p = std::malloc(1024);
    if (!p) return 0;
    // POSIX permits realloc(p,0) to free p and return NULL.
    // ly_realloc frees again on NULL, causing double-free.
    (void)ly_realloc(p, 0);
    return 0;
}

stdout

=================================================================
==1==ERROR: AddressSanitizer: attempting double-free on 0x519000000080 in thread T0:
    #0 0x558a56a35616 in free (/fuzz/test+0x118616) (BuildId: 3cdbcee87f4049a203614f4880bbaf914dd8e139)
    #1 0x558a56a74754 in ly_realloc /fuzz/src/src/ly_common.c:98:9
    #2 0x558a56a74657 in main /fuzz/testcase.cpp:11:11
    #3 0x7fd8f7ff9d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #4 0x7fd8f7ff9e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #5 0x558a56999514 in _start (/fuzz/test+0x7c514) (BuildId: 3cdbcee87f4049a203614f4880bbaf914dd8e139)

0x519000000080 is located 0 bytes inside of 1024-byte region [0x519000000080,0x519000000480)
freed by thread T0 here:
    #0 0x558a56a35ce5 in realloc (/fuzz/test+0x118ce5) (BuildId: 3cdbcee87f4049a203614f4880bbaf914dd8e139)
    #1 0x558a56a7473c in ly_realloc /fuzz/src/src/ly_common.c:96:15
    #2 0x558a56a74657 in main /fuzz/testcase.cpp:11:11
    #3 0x7fd8f7ff9d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

previously allocated by thread T0 here:
    #0 0x558a56a358be in malloc (/fuzz/test+0x1188be) (BuildId: 3cdbcee87f4049a203614f4880bbaf914dd8e139)
    #1 0x558a56a74628 in main /fuzz/testcase.cpp:7:15
    #2 0x7fd8f7ff9d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: double-free (/fuzz/test+0x118616) (BuildId: 3cdbcee87f4049a203614f4880bbaf914dd8e139) in free
==1==ABORTING

stderr


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/CESNET/libyang /fuzz/src && \
    cd /fuzz/src && \
    git checkout efe43e3790822a3dc64d7d28db935d03fff8b81f && \
    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 build dependencies
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    build-essential \
    pkg-config \
    cmake \
    ninja-build \
    libpcre2-dev \
    libxxhash-dev \
 && rm -rf /var/lib/apt/lists/*

# Configure, build, and install libyang (static library preferred)
WORKDIR /work/build
ENV CC=clang_wrapper CXX=clang_wrapper++
RUN cmake -G Ninja /fuzz/src \
    -DCMAKE_C_COMPILER=clang_wrapper \
    -DCMAKE_CXX_COMPILER=clang_wrapper++ \
    -DCMAKE_INSTALL_PREFIX=/fuzz/install \
    -DBUILD_SHARED_LIBS=OFF \
    -DENABLE_TOOLS=OFF \
    -DENABLE_TESTS=OFF \
    -DENABLE_YANGLINT_INTERACTIVE=OFF \
 && ninja -j"$(nproc)" && ninja install

Build Command

clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lyang -lpcre2-8 -lxxhash -lpthread -ldl -lm && /fuzz/test

Reproduce

  1. Copy Dockerfile and testcase.cpp into a local folder.
  2. Build the repro image:
docker build . -t repro --platform=linux/amd64
  1. 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 -lyang -lpcre2-8 -lxxhash -lpthread -ldl -lm && /fuzz/test"


Additional Info

This testcase was discovered by STITCH, an autonomous fuzzing system. All reports are reviewed manually (by a human) before submission.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions