Automated Threat Modeling

Overview of CVE-2026-105437: Heap Buffer Overflow in Git's xdiff Three-Way Merge via Oversized Conflict Markers

A brief summary of CVE-2026-105437, a heap buffer overflow in Git's xdiff three-way merge code where a committed conflict marker size can wrap a 32-bit output size counter. It covers the root cause, the forge mergeability exposure, the upstream patch series, and fixed versions.

CVE Analysis

8 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-10

Overview of CVE-2026-105437: Heap Buffer Overflow in Git's xdiff Three-Way Merge via Oversized Conflict Markers
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A small branch pushed to a Git forge could be enough to make the server write gigabytes of attacker-controlled data past the end of a heap buffer, simply because the forge checked whether a merge request was mergeable. The trigger is a single committed .gitattributes line that inflates conflict markers until Git's 32-bit merge output counter wraps around and the resulting allocation is far too small.

The issue is tracked as CVE-2026-105437, classified as CWE 122 (Heap-based Buffer Overflow), and rated CVSS 3.1 7.5 (High) by GitLab Inc., which acted as CNA. The affected product is upstream Git, not a GitLab-specific component. The documented minimum impact is a crash. Because the overflow is large and its contents are largely attacker-controlled, the advisory notes that worse heap corruption is possible. Reliable code execution has not been demonstrated in the public material.

Technical information

Root cause: two passes that must agree on one 32-bit total

Git's textual merge output is produced by xdl_fill_merge_buffer() in xdiff/xmerge.c, and that function runs twice:

  1. The first call passes dest == NULL and only adds up how many bytes the merge result needs.
  2. The caller then calls malloc() with that number.
  3. The second call walks the same hunks again and writes each piece at dest + size.

This design is only safe if both passes agree on the total. Before the fix, that total was an int, which is 32 bits even on 64-bit platforms.

The amplifier is the conflict-marker-size attribute. It can be set from a committed .gitattributes file, it accepts any positive integer, and the marker length is added several times for every conflict (the opening, base, separator and closing markers). A small file with a huge marker size and many separate conflicts can therefore push the real output size past 4 GiB. In the words of the fix commit, the sum "wraps back to a small positive value", malloc() succeeds with a buffer that is too small, and the write pass runs past the end of the buffer and later lands before its start.

For defenders, the important property is that input size and output size diverge. The data an attacker pushes is small, and the oversized output only materializes during the merge.

Concrete trigger shape

The upstream regression tests commit a .gitattributes with conflict-marker-size=3495254 and create two branches, each changing every fifth line of a 2,100-line file. That yields 420 separate conflicts, and the merged file comes out just over 4 GiB, enough to exceed a 32-bit counter.

Attack flow

Based on the advisory, exploitation follows this sequence:

  1. Control a repository. The attacker commits a .gitattributes entry setting a very large conflict-marker-size for a target file.
  2. Prepare conflicting branches. Two branches modify many separate regions of the same small file so a merge produces many independent conflicts.
  3. Cause a textual three-way merge. Any command that does this can trigger the bug, including git merge, git rebase, git cherry-pick and git merge-tree.
  4. Sizing pass wraps. The 32-bit total overflows to a small value and malloc() returns an undersized buffer.
  5. Write pass overflows. Attacker-controlled merge output, mostly marker bytes and conflicting content, is written outside the buffer.

The advisory singles out Git forges as the most likely target. Forges may run git merge-tree on the server to check mergeability. When the server reads attributes from trees in the repository (for example through attr.tree or --attr-source), any user who can push branches and request that they be merged, for example by opening a pull or merge request, can trigger the overflow without anyone else doing anything.

Reading the CVSS vector

The published vector is:

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

The vector includes UI:R, while the advisory text separately describes a server workflow that needs no action from another person. We read these as two different interaction contexts: client-side merges of an attacker repository, and unattended forge mergeability checks. Exposure should be assessed by whether your deployment performs textual merges on untrusted content and sources attributes from untrusted trees, not simply by whether Git is installed.

Exploitation status

The CVE record credits taise for reporting the issue through HackerOne report 4044249, tagged technical-description, exploit and permissions-required. The report body is not publicly accessible. The reviewed sources identify no threat actor, campaign or observed intrusion, and as of November 10, 2026 the CVE is not in CISA's Known Exploited Vulnerabilities catalog. A public, validated proof of concept is not confirmed.

Patch information

Upstream Git fixed this in a three-commit series by Kaartic Sivaraam, based on a patch by Jeff King and committed by Johannes Schindelin. It shipped in Git 2.56.1, with backports to 2.55.1, 2.54.1, 2.53.1, 2.52.1, 2.51.3 and 2.50.2. The 2.56.1 release notes list CVE-2026-105437 among the six CVEs fixed in this round. GitHub's advisory summarizes the fix as moving to appropriately sized types with checked arithmetic, so that merges whose output size cannot be represented safely are rejected.

Commit 1: 0dc3b5c, a five-operand overflow helper

Git already has st_add(), st_add3() and st_add4() in git-compat-util.h. These perform size_t addition and stop Git with an error instead of wrapping. One line in fill_conflict_hunk() adds five terms together, so the patch adds a five-operand version:

#define st_add3(a,b,c) st_add(st_add((a),(b)),(c)) #define st_add4(a,b,c,d) st_add(st_add3((a),(b),(c)),(d)) +#define st_add5(a,b,c,d,e) st_add(st_add4((a),(b),(c),(d)),(e))

Commit 2: 288266c, "xdiff: use size_t for merge buffer size computations"

This commit changes the size-tracking variables and return types from int to size_t across xdl_recs_copy_0(), xdl_recs_copy(), xdl_orig_copy(), fill_conflict_hunk() (both its size parameter and its return value) and xdl_fill_merge_buffer():

-static int xdl_fill_merge_buffer(xdfenv_t *xe1, const char *name1, ... +static size_t xdl_fill_merge_buffer(xdfenv_t *xe1, const char *name1, ... - int size, i; + size_t size; + int i;

Because size_t is unsigned, the running total can no longer go negative, and on 64-bit systems it cannot realistically wrap. One more problem remained. The result goes into mmbuffer_t, whose size field is a long, and a long is only 32 bits on Windows. Rather than truncate silently, xdl_do_merge() now gives up when the size will not fit:

- int size = xdl_fill_merge_buffer(xe1, name1, xe2, name2, + size_t size = xdl_fill_merge_buffer(xe1, name1, xe2, name2, ancestor_name, favor, changes, NULL, style, marker_size); + if (size > LONG_MAX) { + xdl_cleanup_merge(changes); + return -1; + } result->ptr = xdl_malloc(size);

The merge then fails with an error that callers already handle. The commit message notes that a full move of mmbuffer_t to size_t is planned as separate work.

Commit 3: b131387, "xdiff: check for overflow when computing merge buffer size"

Commit 2 left two gaps:

  1. Marker terms such as marker_size + 1 + needs_cr + marker1_size were still summed as int before being added to size. With conflict-marker-size close to INT_MAX, that inner sum goes negative. Converted to size_t and added, it makes size smaller, even on 64-bit systems.
  2. On 32-bit platforms, size_t is 32 bits, so the total can still wrap.

The fix routes every addition in the sizing pass through the checked helpers and adds each marker term separately, so the inner int sum never happens:

if (!dest) { - size += marker_size + 1 + needs_cr + marker1_size; + size = st_add5(size, marker_size, 1, needs_cr, marker1_size);
- for (i = 0; i < count; size += recs[i++]->size) + for (i = 0; i < count; size = st_add(size, recs[i++]->size))

The same change applies to the ||||||| and ======= markers (st_add5 and st_add4), the >>>>>>> marker, and every xdl_recs_copy() / xdl_orig_copy() call in fill_conflict_hunk() and xdl_fill_merge_buffer().

The memset(dest + size, ...) lines in the write pass deliberately keep plain +=. Once the sizing pass finishes without error, the write pass repeats exactly the same additions into a buffer of exactly that size, so it cannot overflow. As the commit puts it: "We need only perform the overflow checks on the first round."

How the fix is tested

Both commits add tests to t/t4300-merge-tree.sh, marked EXPENSIVE, using the 420-conflict setup described above, whose merged output is just over 4 GiB. Expected results by platform:

PlatformExpected result
64-bit size_t and longgit merge-tree --write-tree finishes cleanly, reports a conflict (exit code 1) and produces a valid tree
64-bit size_t, 32-bit long (Windows)Merge fails with failure to merge
32-bit size_tMerge fails with size_t overflow

As a result, merge, rebase, cherry-pick and server-side merge-tree now stop with an error instead of writing past the end of the heap buffer.

Interim workarounds for servers

Servers that cannot upgrade right away have two options from the advisory. The first is to add this line to each repository's info/attributes:

* conflict-marker-size=7

Git's attribute documentation confirms that $GIT_DIR/info/attributes takes precedence over committed .gitattributes, and that the usual conflict marker length is 7 characters, so this neutralizes the malicious attribute at a higher-precedence layer. The second is to turn off server-side mergeability checks for untrusted repositories. Both constrain the trigger without fixing the arithmetic, so upgrading remains the actual correction.

Affected systems and versions

The CVE record lists the following affected ranges and fixed versions. Release-candidate starting points appear exactly as published:

Affected rangeFixed version
Versions before 2.49.12.49.1
From 2.50.0-rc0, before 2.50.22.50.2
From 2.51.0-rc0, before 2.51.32.51.3
From 2.52.0-rc0, before 2.52.12.52.1
From 2.53.0-rc0, before 2.53.12.53.1
From 2.54.0-rc0, before 2.54.12.54.1
From 2.55.0-rc0, before 2.55.12.55.1
From 2.56.0-rc0, before 2.56.12.56.1

These are separate boundaries per release line, so "anything before 2.56.1" is not the correct test. The upstream patch announcement lists backports to 2.50.2 through 2.55.1 alongside 2.56.1, while the CVE record additionally lists 2.49.1. Confirm the exact build you run against your distribution or vendor packaging.

Vulnerable configurations:

  • Any Git client performing a textual three-way merge of attacker-influenced content via git merge, git rebase, git cherry-pick or git merge-tree.
  • Git forges and hosting servers that run git merge-tree for mergeability checks and read attributes from repository trees (for example via attr.tree or --attr-source). This is the configuration where a user who can push and open a merge request can trigger the overflow unattended.

Git's security policy also states that older maintenance tracks have no formal guarantee of security updates, so lines not listed above should be treated as needing an upgrade to a fixed track.

Vendor security history

Git has had prior integer-related memory-safety bugs. GitLab describes an audit by X41 on behalf of OSTIF that identified CVE-2022-41903 in Git's formatting code (pretty.c), where a reproducer exceeded what an int could hold and triggered heap corruption. That finding went through the private Git security list, and GitLab Gitaly engineer Patrick Steinhardt developed fixes, found additional issues and extended fuzzing coverage. Separately, Git's July 2025 security announcement shipped fixes across multiple maintenance releases for vulnerabilities including CVE-2025-48384.

The pattern is a familiar one. int-sized arithmetic on attacker-influenced lengths gets handled through a coordinated private disclosure process, with fixes across several maintenance lines. The CVE-2026-105437 series follows the same approach and reuses the existing st_add family rather than inventing new checks.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization