On May 22, 2026, the GitHub Security Lab published advisory GHSL-2026-140, disclosing a heap buffer overflow in 7-Zip's NTFS archive handler. The report was submitted privately on April 24, 2026, and a patch shipped three days later in version 26.01, but the public advisory did not land until nearly a month after the fix. An attacker who can convince a user to open a crafted file in 7-Zip can achieve arbitrary code execution with no administrative privileges and no specific file extension required. CVE-2026-48095 carries a CVSS 8.8 HIGH score, and the bug has existed in the codebase since NTFS compressed stream support was first added, meaning every 7-Zip installation was potentially exposed until April 27, 2026. The tool has been downloaded 428 million times from SourceForge alone.
This is a two-layered breakdown. Part I is written for engineering managers, security leads, and architects who need to decide whether to act and what to tell their teams. Part II reconstructs the full exploitation chain the way Jaroslav Lobacevski of the GitHub Security Lab did: from the one-line function that mis-sizes a buffer, through the 256 MB write that corrupts a vtable, to the moment a second read dispatches execution through a hijacked pointer. The article closes with how Pragma Core addresses exactly this class of problem.
Part I. Executive breakdown
What happened
7-Zip processes dozens of archive formats, and one of those formats is NTFS, the file system used by Windows. Support for NTFS compressed streams has been part of 7-Zip for years, and the handler that manages it contains a function called GetCuSize(). That function's job is to compute how large a buffer to allocate before reading compressed data from disk.
The problem is a single line of arithmetic. When the values that NTFS metadata provides reach a specific combination (a cluster size log of 28 and a compression unit of 4), the buffer size calculation produces an exponent of 32. Shifting a 32-bit integer left by 32 positions is undefined behavior in C++. In practice, on x86 and x64 processors, the hardware quietly masks the shift count, so the result is 1 instead of the 4 gigabytes the code intended to express. A 1-byte buffer gets allocated.
Then 7-Zip tries to read data into that buffer. The first read call writes up to 256 megabytes of data from the crafted NTFS image into the 1-byte buffer. The memory immediately adjacent to that buffer, which belongs to the stream object's vtable pointer, gets overwritten with attacker-controlled bytes. The next time 7-Zip calls a virtual method on that object, the processor follows the hijacked pointer straight into attacker-controlled code.
Who is affected
| Component | Status |
|---|---|
| 7-Zip 26.00 and all earlier versions | Vulnerable. Upgrade to 26.01 immediately. |
| 7-Zip 26.01 | Patched. |
Embedded 7z.dll in third-party installers and tools |
Vulnerable if the bundled version is 26.00 or earlier. Requires individual verification. |
The attack requires no particular file extension. 7-Zip uses a signature-based fallback: when a file fails to match its extension-specific handler, 7-Zip tries all remaining handlers by signature priority. The NTFS handler triggers on the magic bytes "NTFS " at byte offset 3 regardless of whether the file is named .7z, .zip, .rar, .pdf, or anything else. A phishing email delivering a file called Q2_report.zip is a fully valid delivery vehicle. 7-Zip has been downloaded 428 million times from SourceForge alone since 2002, and is also bundled as 7z.dll inside a wide range of enterprise installers and software distribution tools, which means the actual count of vulnerable installations exceeds any single download figure.
Why this matters beyond 7-Zip
The GetCuSize() shift is a canonical example of a class of bug that C++ developers routinely underestimate: undefined behavior that compiles without warning, runs without sanitizer coverage, and produces an outcome determined by hardware rather than the C++ standard. The result looks perfectly fine in a code review until someone thinks to ask what happens when the exponent reaches the integer's bit width.
The extension-agnostic attack surface compounds the exposure. Most organizations filter on file extension at email gateways and web proxies. This attack bypasses that layer entirely because the NTFS handler activates on file content rather than file name. The practical delivery method is indistinguishable from a legitimate archive attachment. Any organization relying solely on extension-based filtering for archive files has no control at the gateway level against this vector.
Recommended actions
- Upgrade every 7-Zip installation to version 26.01 or later. On Windows, check both per-user and system-wide installations separately.
- Audit your software portfolio for bundled copies of
7z.dll. Any application that ships its own copy of 7-Zip's DLL must be updated independently of the standalone 7-Zip install. - At email and web gateways, add a YARA rule to inspect archive file content for the NTFS signature bytes (
4E 54 46 53 20) at byte offset 3 within incoming files. - Review log records for unusual 7-Zip crash reports or application hangs, which may indicate failed exploitation attempts.
- If an immediate upgrade is not possible, consider sandboxing or restricting 7-Zip access for files received from untrusted sources.
- Notify downstream teams responsible for software that bundles
7z.dlland track their patching status separately.
Part II. Technical breakdown
Background: 7-Zip's NTFS handler and compressed attributes
7-Zip supports reading raw NTFS disk images as an archive format, not just archives placed on an NTFS volume. The handler parses the NTFS boot sector, enumerates Master File Table (MFT) records, and extracts file data from data run lists. For files stored with NTFS compression, the handler reads and decompresses individual compression units, each of which is a fixed number of clusters on disk.
The relevant data structures come from two places: the boot sector, which provides BPB_SecPerClus encoding the cluster size; and the $DATA attribute of MFT records, which carries a CompressionUnit field controlling how many clusters form one compression unit. Both values feed directly into GetCuSize(), which computes the byte size of a single compression unit. The result becomes the allocation size for the input and output staging buffers used during decompression.
The design invariant being violated is simple: the computed size must fit in a UInt32, and the result must be large enough to hold actual compression-unit data. When the shift overflows, both requirements fail silently.
The vulnerability: undefined-behavior shift in GetCuSize()
The vulnerable function is one line inside NtfsHandler.cpp:
UInt32 GetCuSize() const {
return (UInt32)1 << (BlockSizeLog + CompressionUnit);
}
BlockSizeLog encodes the cluster size. NTFS allows cluster sizes up to 2 to the power of 30 bytes, so the parser enforces a guard:
if (ClusterSizeLog > 30)
return false;
This guard permits ClusterSizeLog values of 28, 29, and 30. A crafted NTFS boot sector with SectorsPerCluster = 0xED encodes ClusterSizeLog = 28. Non-resident compressed data attributes explicitly permit CompressionUnit == 4, meaning 16 clusters per compression unit.
When BlockSizeLog == 28 and CompressionUnit == 4, the shift expression becomes (UInt32)1 << 32. The C++ standard declares that left-shifting a value of type UInt32 by 32 or more bits is undefined behavior. On x86 and x64, the processor implements shift count modulo 32, so the effective operation is (UInt32)1 << 0, which yields 1.
Under Clang with UndefinedBehaviorSanitizer enabled, the runtime flags this immediately:
shift exponent 32 is too large for 32-bit type 'UInt32'
The function returns 1 instead of 4,294,967,296. Every downstream allocation uses this corrupted size.
Root cause: two under-allocated buffers and a vtable in range
Two heap buffers are allocated using the return value of GetCuSize():
_inBuf: the compressed input staging buffer, allocated as 1 byte_outBuf: the decompressed output buffer, allocated as 8 gigabytes on 64-bit systems with sufficient RAM (because the output size computation also derives from the same corrupted base value)
On a 64-bit system with 16 GB or more of RAM, the 8 GB _outBuf allocation succeeds. The heap layout places the CInStream object (304 bytes) immediately after _inBuf in the allocator's free list.
The read loop then calls ReadStream_FALSE(_inBuf, GetCuSize()), which attempts to fill _inBuf with up to 256 MB of NTFS cluster data from the crafted image. The very first read call overflows the 1-byte boundary after one byte. At byte offset 304 from _inBuf, the write reaches the CInStream object's vtable pointer field and overwrites it with attacker-controlled bytes from the NTFS data run.
On 32-bit builds, the undefined behavior in the shift expression also corrupts the _outBuf allocation, and the overflow is reached unconditionally regardless of available memory. On 64-bit systems with less than 16 GB of RAM, the 8 GB allocation fails and the result is a denial of service rather than code execution.
Exploitation: vtable hijack through a crafted compression unit
The exploitation flow proceeds in two read iterations:
First read. ReadStream_FALSE(_inBuf, GetCuSize()) begins writing NTFS cluster data from the crafted image. After 304 bytes of overflow, the write has crossed from the 1-byte _inBuf into the adjacent CInStream object. The vtable pointer field is overwritten with bytes that appear at the corresponding position in the NTFS data run. The attacker controls the contents of data runs through the MFT record structure.
Second read. The decompression loop iterates and calls a virtual method on the now-corrupted CInStream object. The processor dereferences the hijacked vtable pointer and dispatches to wherever the attacker has directed it.
Constructing a reliable exploit requires knowing the target process's memory layout well enough to place a usable gadget or shellcode address in the vtable slot overwrite position. This depends on the ASLR and DEP configuration of the target system and the heap allocator's layout behavior. The public PoC demonstrates reliable crash; a weaponized exploit requires additional target-specific work.
The public PoC generator, gen_ntfs_sparse.py, creates a 512 MB sparse NTFS image that is approximately 8 KB of actual disk data. The image contains:
- A boot sector with
SectorsPerCluster = 0xED, encodingClusterSizeLog = 28 - Seven MFT records at byte offset 256 MB within the sparse file, each containing correct Update Sequence Number (USN) fixup arrays required by the NTFS parser
- A compressed
$DATAattribute withCompressionUnit = 4and a runlist directing 7-Zip to read from the crafted data region
The PoC demonstrates reliable crash on 7-Zip 26.00 and triggers the vtable corruption on 64-bit systems. The PoC was published alongside the advisory on May 22, 2026.
Affected versions
- 7-Zip 26.00 and all versions prior: vulnerable. The
GetCuSize()computation was introduced when NTFS compressed stream support was added and has not changed since. - 7-Zip 26.01 (released April 27, 2026): patched.
No Linux or macOS packages are immune if they ship 7-Zip 26.00 or earlier. The underlying C++ undefined behavior is platform-independent; the impact (RCE versus DoS) depends on the memory configuration of the host.
Timeline
| Date | Event |
|---|---|
| 2026-04-24 | Jaroslav Lobacevski submits GHSL-2026-140 via SourceForge private issue |
| 2026-04-27 | 7-Zip 26.01 released with the patch |
| 2026-05-22 | GitHub Security Lab publishes the public advisory |
| 2026-05-26 | CVE-2026-48095 published and widely reported |
| 2026-05-29 | Public PoC (gen_ntfs_sparse.py) available |
A note on the discovery methodology
Jaroslav Lobacevski found this bug through targeted code review of 7-Zip's less-traveled format handlers, combined with dynamic analysis under Clang's UndefinedBehaviorSanitizer on Linux x64. UBSan is one of the most effective tools for exactly this category of bug: code that passes every conventional test, compiles without warning, and looks correct in review, but violates a C++ contract at a specific input boundary that only adversarial inputs reach.
Most fuzzers would have difficulty reaching the NTFS handler without format-aware, structure-guided inputs. The handler activates on an NTFS signature, not on a common archive extension, which means random-byte fuzzing almost never produces a valid trigger. Structured fuzzing of NTFS boot sector fields, or manual derivation of the triggering field combination as Lobacevski did, is necessary.
The three-day turnaround between private report and patch reflects both the simplicity of the fix (the shift expression needs a guard against exponents of 32 or more) and the direct relationship the GitHub Security Lab maintains with upstream maintainers. It also means that anyone monitoring the 7-Zip changelog could have inferred the bug class from the diff alone before the advisory was published.
What we should learn from CVE-2026-48095
-
Undefined behavior is a silent contract violation, not a compilation error. The shift expression in
GetCuSize()compiles cleanly at default warning levels. UndefinedBehaviorSanitizer must be part of the CI pipeline for any C or C++ codebase, run against a realistic corpus of format inputs and not just unit tests. An automated run of UBSan against a set of crafted archive inputs would have caught this before any release. -
Single-line size computations that feed allocators deserve explicit overflow analysis. One-liner arithmetic where both operands come from attacker-controlled fields and the result goes directly to a heap allocation is a high-risk pattern regardless of the surrounding code complexity. These lines need bounds checks, and they are natural targets for manual review and static analysis rules.
-
Signature-based format detection widens every handler's attack surface. When a tool accepts dozens of formats and falls back to signature matching after extension rejection, every format handler becomes reachable through any file. The NTFS handler was not considered part of the attack surface for
.zipfiles. It was. Format parsers that use signature-based dispatch need their threat models to reflect this. -
Bundled native libraries are a hidden exposure surface that package managers do not track. The
7z.dllbundling pattern means the number of vulnerable installations is larger than the 7-Zip download count suggests. Any software that ships its own copy of 7-Zip's compression library carries its own separate patch obligation, independent of what users do with the standalone tool. -
A working PoC shifts the risk timeline immediately.
gen_ntfs_sparse.pywas published alongside the advisory. The gap between public advisory and weaponized exploit is now a question of attacker effort to build the vtable redirect chain, not a question of whether the primitive exists. Organizations with unpatched installations should treat this as an active risk.
How Pragma Core addresses this class of problem
CVE-2026-48095 is the kind of vulnerability that classical security scanners walk past without flagging. The GetCuSize() function is four tokens and a semicolon. There is no injection sink, no dangerous system call in direct scope, and no obvious taint flow. The bug is about what the function returns under a specific input condition and what the caller does with it two frames up the stack. Pragma Core is built for exactly this class of issue: vulnerabilities that live in the relationship between components rather than in any single line.
SAST tuned for integer-to-allocation patterns
Off-the-shelf static analysis is tuned for taint flows ending at SQL or shell sinks. It misses logic bugs, authorization failures, and allocation-size corruption. Pragma Core's SAST can be tuned to a different pattern: arithmetic expressions sourced from attacker-controlled fields that feed directly into heap allocation size arguments with no intervening bounds check. The GetCuSize() shift feeding _inBuf.Alloc(GetCuSize()) is a high-confidence instance of this pattern. A rule that flags unchecked shift expressions used as allocation sizes, where the operands trace back to external input, would produce a finding that names this line directly.
Autonomous AI agents for attack chain investigation
Pragma Core's autonomous agents reason across call chains rather than stopping at a flagged line. For a bug like this one, the agent asks: what is the maximum value this argument can reach, who controls the inputs to that computation, and what is the consequence if the result is wrong? Applied to GetCuSize(), the agent would trace BlockSizeLog and CompressionUnit back to their origin in the NTFS boot sector and MFT attribute, confirm both are attacker-controlled, and then follow the allocation result forward to ReadStream_FALSE to determine what happens when the buffer is smaller than the data being written. That is the full chain, surface to impact, as a single automated investigation.
Interactive call graphs with vulnerability overlay
The relationship between GetCuSize(), the allocator, and ReadStream_FALSE is invisible when reviewing any one file in isolation. Pragma Core auto-generates call graphs for connected repositories and overlays findings, making the data flow visible at a structural level. In this case, the relationship between the size computation and the unbounded write that follows is the entire vulnerability. A call graph view makes that relationship obvious rather than hidden across three levels of call indirection.
Continuous tracking of third-party packages and bundled components
The 7z.dll bundling issue is a dependency tracking problem that most tools miss. Pragma Core tracks third-party packages and bundled native libraries across all connected repositories, including libraries that are vendored rather than installed through a package manager. When CVE-2026-48095 landed in the catalog, any team with a repository that embeds 7z.dll would receive a notification with the CVSS score and the fixed version, rather than discovering the exposure weeks later through a manual audit of installer scripts.
Full SBOM per repository
Pragma Core generates a complete component inventory per repository, exportable as CycloneDX JSON. For organizations asking which products ship a copy of 7-Zip and at what version, that answer comes from the SBOM catalog rather than from a multi-day manual survey of build scripts and deployment packages. The question of scope, which every incident starts with, becomes an automated lookup rather than an investigation.
Human-guided AppSec investigations
The investigation Jaroslav Lobacevski ran on 7-Zip's NTFS handler, targeted code review of format parsers combined with sanitizer-assisted dynamic analysis, is precisely what Pragma Core's expert-led research module supports for any connected codebase. A security team that suspects a niche format handler or any other parser-heavy subsystem is fragile can direct Pragma Core's agents to investigate that surface specifically, backed by the platform's existing code map and call graph context, without commissioning a separate engagement.
Closing thoughts
CVE-2026-48095 is not, fundamentally, a bug about 7-Zip. It is a bug about the assumption that correct-looking arithmetic is correct arithmetic in C++, and about what happens when a format parser's undefined behavior corner is reached by an attacker before the security team reaches it with a sanitizer. That pattern lives in memory allocators, format parsers, cryptographic length calculations, and firmware update handlers across almost every system written in C or C++. The shift expression in GetCuSize() is representative of an entire class of one-liners that are quietly wrong in a way that only a sanitizer run or an adversarial input will reveal.
The extension-agnostic delivery angle is equally instructive beyond 7-Zip. Signature-based dispatch is a convenience that parser libraries routinely rely on, and it turns every supported format's attack surface into a surface reachable through any file. Security teams modeling threats based on file extensions are working from an incomplete picture of their actual exposure.
Organizations that want to move from "we scan and report" to "we systematically investigate what is fragile" can reach Pragma Core at pragma-core.com for a demo.
Sources
- Jaroslav Lobacevski, GitHub Security Lab, GHSL-2026-140: Heap Buffer Write Overflow in 7-Zip, May 22, 2026.
- SOC Prime, CVE-2026-48095: 7-Zip Heap Overflow Flaw, May 2026.
- Security Online, 7-Zip Heap Buffer Overflow: CVE-2026-48095 Details, May 2026.
- The CyberSec Guru, CVE-2026-48095: 7-Zip Heap Buffer Overflow Vulnerability, May 2026.
- York University ITS, 7-Zip Heap Buffer Overflow (CVE-2026-48095), May 2026.
- NVD, CVE-2026-48095 Detail.