Vulnerabilities

7-Zip NTFS Parser One-Byte Buffer (CVE-2026-48095)

CVE-2026-48095 joins an oversized NTFS cluster exponent to a compressed-attribute value inside 7-Zip, so a 32-bit shift collapses the input allocation to one byte in the tested builds before one extent can supply 256 MiB; every parser copy through 26.00 should move to the current official 26.02, or at minimum 26.01 or a backport verified against the same source invariant.

A warm hand-drawn forensic bench where an NTFS disk ruler and a compression token feed a shift mechanism above a tiny cup and a broad data stream.
In this article

Research basisfixed-object review of 7-Zip tags 26.00 , 26.01 , 26.02 , GHSL-2026-140, and the NTFS stream implementation

SourceGitHub Security Lab / 7-Zip source / Microsoft NTFS documentation / SOSEC source review

The default action is direct: find every 7-Zip executable, library, plug-in, and private copy that can receive untrusted files, then replace releases through 26.00 with the current official 26.02. Workers that cannot be replaced immediately stop recursive testing and extraction and route NTFS content to a corrected isolated service. A rollback may target another verified fixed object, never 26.00.

The flaw occurs before decompression. The boot sector supplies the volume-wide cluster scale, while a non-resident attribute supplies the compression-unit scale. Version 26.00 accepts each value locally; they first share a 32-bit shift when CMftRec::GetStream() constructs CInStream. The tested x86/x64 builds allocate one input byte, then retain a 256 MiB extent-read length derived from the volume scale.

This report pins 26.00, the first fixed release 26.01, and 26.02, which remained the official current release on July 26, 2026. Source establishes the bad arithmetic, read relationship, and repaired control flow. Whether a specific host was exposed, crashed, or compromised still depends on the object it actually loaded, input provenance, process state, and behavior after parsing.

1 Three Values Explain the Entire Failure Chain

1.1 Volume scale, compression scale, and extent length meet in one stream

The first value comes from the boot sector. In 26.00, CHeader::Parse() lines 122–134 add the sector exponent and sectors-per-cluster exponent and reject only ClusterSizeLog > 30. The public case selects 28, so one cluster represents 2^28 bytes, or 256 MiB.

The second value comes from a non-resident $DATA attribute in the MFT. Lines 513–520 read CompressionUnit from attribute offset 0x22, and lines 424–430 allow zero or four. Four means that one compression unit contains sixteen clusters.

The third step is stream construction and reading. CMftRec::GetStream() lines 1218–1234 copy the cluster exponent into BlockSizeLog and pass the attribute value to InitAndSeek(). Lines 680–697 evaluate (UInt32)1 << (28 + 4). C++ leaves that 32-bit shift undefined; the tested builds produce one and allocate _inBuf accordingly. Lines 934–947 then convert one physical cluster to 256 MiB and pass that length directly to ReadStream_FALSE().

A warm hand-drawn diagram follows a volume-scale token and an attribute-compression token through separate rooms until they meet inside a stream object above two allocation trays.
Figure 1. The boot parser supplies a cluster exponent, the attribute parser supplies a compression exponent, and GetStream() combines them while extent reading continues to use the volume scale.

A benign worked example fixes the units. A 512-byte sector has exponent 9. Eight sectors per cluster add 3, producing ClusterSizeLog = 12 and a 4 KiB cluster. Adding CompressionUnit = 4 yields exponent 16 and a 64 KiB compression unit. The public case changes the first result to 28, making the combined 32-bit shift invalid while one extent still retains a 256 MiB physical-read scale.

LZNT1 runs after this read. Physical extents must first populate _inBuf, and only then does the code call Lznt1Dec(); compressed-token validity cannot protect an undersized input object. The x86 path also forms a tiny output allocation, while x64 requests an 8 GiB output cache first. Resource exhaustion can stop x64 earlier, but the source-level input-capacity/read-length relationship remains invalid.

1.2 Fix boundary and the single response map

Code and response. The affected range is 7-Zip 26.00 and earlier. Commit 8c63d71ff886bda90c86db28466287f977374237 in the first fixed release, 26.01, changes the ceiling from 30 to 21 at the same source lines 122–134. As of July 26, 2026, the official current release was 26.02; commit f9d78aff31a5f2521ae7ddbdc97c4a8855808959 still enforces the check at lines 122–134. An equivalent backport must prove the same control-flow property: ClusterSizeLog > 21 exits before MFT seeking, CInStream construction, allocation, or reading, and every path into GetStream() inherits that validated header state. A capacity-aware offs + compressed <= _inBuf capacity check is required defense in depth; it cannot authorize geometry outside the upstream-supported domain.

OwnerObjectActionAcceptance evidenceFailure handling and exit
Parser or desktop ownerEvery executable, 7z.dll, plug-in, container, and private copyPrefer 26.02; use 26.01 or a proved equivalent backport where constrainedLoaded path, architecture, and hash map to a trusted package and source propertyOn compatibility failure, route to another verified fixed pool; exit after every reachable copy is verified
Ingress or archive-service ownerInputs with the eight-byte NTFS signature at offset three, plus ambiguous formatsRoute content to a corrected isolated service; pause recursive testing and extraction on vulnerable workersHandler selection, routing result, worker hash, and exception expiryRate-limit or disable the feature when isolation capacity fails; never fall back to 26.00
Validation ownerHarmless header-only samples at cluster exponents 20, 21, and 22, plus normal NTFS corpusLet 20/21 reach the next valid stage; reject 22 in the header before MFT, allocation, or readExact branch record, no-allocation/read probes, and expected list/test/extract hashesStop promotion if 22 reaches downstream code or normal input regresses; exit after correction and rerun
Incident-response ownerSuspect image, parent container, crash, and post-parser behaviorPreserve originals, apparent/allocated sizes, hashes, and provenance before correlating process, module, child-process, file, and network telemetryA traceable timeline from object to job, process, loaded module, and later behaviorTreat post-parser behavior as compromise; close only with the evidence boundary and retention window stated

The public chronology fixes the version boundary: private report on April 24, 2026; 26.01 on April 27; GHSL-2026-140 on May 22; the CNA record on June 5; and 26.02 on June 25. The CNA records 26.00 and earlier as affected and assigns CVSS 3.1 8.8. Public records do not exhaust the earliest vulnerable release or every downstream integration, so inventory must resolve actual binaries and runtime-loaded identities.

Evidence strength follows the object. Pinned source proves the arithmetic, write relationship, and repaired branch. The public optimized-Windows debugger session proves one path from source-controlled extent bytes to corrupted indirect dispatch. Delivery, vulnerable parsing, and code execution on a particular asset require separate content, job, crash, and post-parser evidence. Safe regression stops in a read stub when the proposed write end exceeds recorded capacity and never needs to corrupt the heap.

2 The Boot Sector Writes the Scale of the Whole Volume

2.1 One byte becomes a logarithm only after several earlier checks

CHeader::Parse() receives the first sector as a byte array, but it does not declare NTFS after one magic comparison. It first requires the terminal 0x55AA marker, accepts the expected jump-instruction forms, and checks the eight-byte NTFS OEM identifier at offset three. It derives a sector-size logarithm from the 16-bit value at offset eleven and permits only exponents 9 through 12, representing 512-byte through 4096-byte sectors.

The byte at offset thirteen then describes sectors per cluster. NTFS gives this field two forms. Values at or below 0x80 are treated as a power-of-two sector count and passed through GetLog(). Values above 0x80 use a negative-power convention; 7-Zip converts them to an exponent with 0x100 - v. In both forms the parser stores an exponent, not a byte count.

The volume-wide value is a sum: ClusterSizeLog = SectorSizeLog + sectorsPerClusterLog. That representation is efficient because later multiplication by a cluster size becomes a left shift. It also means the parser must reason about the width of every type that will eventually receive that shift. An exponent accepted here is not confined to header display; it becomes an arithmetic parameter for the rest of the handler.

Tag 26.00 rejects the result only when it exceeds 30. The check prevents an immediate UInt32 cluster-size helper from shifting by 31 or more in some calls, but it leaves 28, 29, and 30 available to downstream code. The public case uses 28 because adding the supported compression-unit exponent 4 lands exactly on 32 in GetCuSize(). The boot decision is locally allowed and globally unsafe.

Other header checks continue after the cluster value. Bytes fourteen through twenty must be zero, the media byte must describe a fixed disk, FAT-sector fields must be zero, reserved bytes are constrained, and the 64-bit sector count is capped. The parser calculates NumClusters, reads the physical cluster for $MFT, and derives master-file-table and index-record sizes through their own positive-count or negative-power encodings.

Reaching the data-stream constructor requires a sufficiently coherent NTFS structure, so ordinary mutations often stop at the boot record or first file record. A defensive detector can therefore extract a small set of early fields and classify geometry before running a full archive operation.

The old ceiling nevertheless grants the cluster exponent authority across many later operations. CHeader::ClusterSize() returns (UInt32)1 << ClusterSizeLog. Physical size is NumClusters << ClusterSizeLog. Cluster locations become byte offsets through the same shift. The MFT record size may add the cluster exponent to another logarithm. Extent validation divides or multiplies allocated sizes by it.

A useful audit therefore treats the boot sector as a capability grant. Once CHeader::Parse() accepts the geometry, every downstream helper assumes that “one cluster” can be safely expressed in its chosen integer type and allocation model. A parser contract succeeds only when the values it emits satisfy those consumers, not merely the syntax of the bytes it just read.

2.2 A 256 MiB cluster changes offsets, reads, caches, and expectations together

With ClusterSizeLog = 28, cluster number one begins 256 MiB from the start of the image. A run of two clusters spans 512 MiB. The public proof uses sparse-file behavior so the apparent disk can contain those distant locations without consuming equivalent physical storage on the researcher's filesystem. Apparent size and allocated size are separate evidence fields when such an image is found.

That sparse delivery property does not make the parsed cluster sparse inside NTFS. The run list still tells 7-Zip which virtual clusters map to physical clusters. DataParseExtents() reconstructs those mappings, checks continuity and allocation, and records a sequence of CExtent objects. Later, CInStream::Read() uses the boot-derived exponent to translate extent positions back into byte offsets and byte lengths.

The 256 MiB scale is unusual for supported NTFS. Microsoft's current Windows documentation lists a 2 MiB maximum cluster for Windows 10 version 1709 and later and Windows Server 2019 and later; earlier supported systems used a 64 KiB maximum. A volume declaring 256 MiB clusters is therefore a strong anomaly even before the compressed attribute is parsed. It is still important to label it as parser-detected geometry, not as a volume Windows successfully mounted.

Compression adds a second unit system. Microsoft documents an NTFS compression unit in relation to clusters and caps normal product behavior. In 7-Zip's attribute model, CompressionUnit = 4 represents a block of sixteen clusters. Under ordinary 4 KiB clusters that is 64 KiB, a familiar LZNT1 unit. Under the crafted 256 MiB geometry, the same exponent describes a theoretical 4 GiB unit and pushes the byte-count calculation across the type width.

The read shown in the public validation does not need to fill that entire theoretical compression unit. The extent loop processes physical runs inside the unit. One physical cluster is already 256 MiB, so compressed = (size_t)numChunks << BlockSizeLog produces 256 MiB when numChunks is one. That length is enough to expose the mismatch with a one-byte input buffer.

Keeping the units explicit prevents two common review errors. CompressionUnit is a cluster-based exponent. BlockSizeLog already incorporates sector size and sectors per cluster and expresses the volume cluster in bytes. The vulnerable expression adds those two scales; record size and LZNT1 chunk size do not participate.

A hand-drawn NTFS boot-sector ruler turns sector and cluster marks into a volume-wide scale used by offsets, records, extents, and reads.
Figure 2. The cluster logarithm is learned once at the boot sector and reused as the scale for many later calculations, so its acceptance limit must satisfy every consumer.

The early anomaly suits a lightweight intake prefilter: recognize the NTFS signature, parse sector size and the sectors-per-cluster encoding, calculate ClusterSizeLog with checked arithmetic, and quarantine values above 21 before traversing the MFT. Truncation, invalid encodings, overflow, and unsupported geometry fail with distinct reasons while preserving raw fields and the derived exponent; the prefilter never seeks to declared offsets or reimplements full NTFS.

The parser itself needs more than one constant. Every exponent carries a unit and a proven domain, every shift names the left-operand type, and sums are formed in a wider type before narrowing or shifting. Language width and product memory policy are separate gates, while allocation capacity and every later read endpoint form an explicit relationship that code can assert.

The record-size fields also use a dual encoding. A positive low byte describes a number of clusters and therefore incorporates ClusterSizeLog; a negative form directly describes a power-of-two byte size. 7-Zip then caps MftRecordSizeLog at 12 and requires it to be at least the sector exponent. Recording these domains separately prevents mechanical application of the cluster fix to unrelated record rules.

Every observation records the value with its unit and consuming expression: raw byte, exponent in sectors or bytes, cluster count, byte count, and actual operand type. “28” has no meaning alone. Keeping those fields in one immutable scale record gives code review, runtime diagnostics, and incident analysis the same interpretation.

The domain proof can be recomputed without executing an image: enumerate supported sector exponents and both sectors-per-cluster encodings, derive the cluster interval, add the compression-unit interval in a wider type, and prove the result remains below 32. Physical offsets, record sizes, extent totals, cache counts, and allocation types each record their narrowest width. A wider format value or narrower consumer must fail the old CI inequalities and force a new proof.

3 Field Handoffs Carry Volume State Into Allocation

Chapter 1 pins the source lines for input, handoff, allocation, and reading. This chapter follows the object lifetime to show how a locally coherent NTFS structure sends two independently accepted values into one stream.

3.1 The attribute parser holds compression metadata alone

The boot sector is parsed once for the volume. File content arrives later through MFT records, each containing a sequence of typed attributes. CAttr::Parse() begins with the attribute type and length, checks eight-byte alignment, reads the resident flag, validates an optional UTF-16 name, and separates the resident and non-resident layouts. The compression value relevant to this case exists only in the non-resident form.

A non-resident attribute does not carry its file bytes inline. It describes virtual cluster numbers, allocated and logical sizes, initialized size, a run-list offset, and possibly a packed size. At offset 0x22, 7-Zip stores one byte in CompressionUnit. The parse step confirms that the header is large enough, but it does not yet decide whether a stream using that value can be built.

The later predicate IsCompressionUnitSupported() accepts exactly zero or four. Zero represents the uncompressed/sparse path in this implementation, and four is the NTFS compressed-stream case used by the public research. The check limits the attribute to a small known set. Its parameters omit ClusterSizeLog, leaving the sum of the two exponents unchecked.

The attribute parser knows the local layout and compression byte; the volume header knows the cluster scale. Both local checks can succeed before the combined value violates the machine-level shift constraint. Cross-structure arithmetic needs a combination check when the structures meet.

Multiple attributes can represent one named data stream. CMftRec groups entries into CDataRef ranges, orders non-resident segments by virtual cluster start, and preserves each segment's run-list data. The stream builder counts non-resident entries and requires a consistent group. Those structural checks help the crafted image reach the deep path while leaving the combined exponent unconstrained.

DataParseExtents() then turns the run lists into virtual-to-physical mappings. It checks that the last virtual cluster aligns with the allocated size, that the allocation is a multiple of the cluster size, that segments meet in order, and that physical locations remain within the volume. It also recalculates packed size from nonempty extents. The image must tell a coherent storage story before the unsafe buffer story begins.

The result is an important distinction for detection. A file can be malformed at the semantic level that matters to a particular implementation while remaining internally consistent enough to pass many format checks. Calling the image simply “corrupt” discards the useful features: valid handler signature, acceptable header fields under 26.00, a parseable MFT record, a supported compression value, and an extent map that leads to a large physical read.

It also explains why extension filters provide poor assurance. The dangerous values live inside the volume and attribute structures. Renaming the outer file does not change them, and a container gateway that trusts a declared MIME type may never examine them. The selected parser and derived fields are stronger evidence than the label attached by the sender.

3.2 Stream construction is the rendezvous where the unsafe sum becomes real

CMftRec::GetStream() is the decisive handoff. It receives clusterSizeLog as an argument from the volume header and locates the chosen data-reference group inside the file record. If the group is resident and singular, it can expose an in-memory buffer. If any part is non-resident or the group has multiple segments, the function enters the path that builds CInStream.

The function first demands that every member of that group be non-resident and that the first attribute's compression unit be supported. It allocates a new CInStream, reconstructs extents using the same cluster exponent, copies logical and initialized sizes, retains the underlying image stream, and assigns ss->BlockSizeLog = clusterSizeLog. Only then does it call ss->InitAndSeek(attr0.CompressionUnit).

One line now carries a volume-level decision into an object whose remaining configuration comes from a file-level attribute. BlockSizeLog is 28 in the demonstrated case. The argument is 4. Neither was transformed during the handoff. The constructor-like method stores the second value and immediately computes _chunkSizeLog = BlockSizeLog + CompressionUnit.

The class layout shows the responsibilities that converge here. CInStream tracks virtual and physical positions, sparse-mode state, current remaining bytes, input and output buffers, the source stream, reconstructed extents, two cache tags, the logical file size, initialized size, block exponent, and compression exponent. It is simultaneously an extent reader, decompression cache, and IInStream implementation presented to the archive framework.

GetCuSize() is deliberately short: return (UInt32)1 << (BlockSizeLog + CompressionUnit);. The cast fixes the left operand at 32 bits, so platform pointer width never enters this expression. The x64 input allocation therefore performs the same undefined shift as x86.

CMftRec::GetStream() makes the cross-structure handoff explicit. It copies the volume's validated clusterSizeLog into CInStream::BlockSizeLog, attaches the reconstructed extents and source interface, then calls InitAndSeek(attr0.CompressionUnit) with the attribute value. A reviewer can therefore trace both operands to their source bytes through actual assignments and parameters; no allocator symptom is needed to infer where the values met.

After allocating, the method clears both cache tags, resets sparse and position state, looks at the first extent, converts its physical cluster to a byte position with e.Phy << BlockSizeLog, and seeks the source stream. A successful return tells GetStream() that the object is ready. The exposed IInStream interface carries no internal input capacity, leaving the archive layer unable to see its disagreement with the future extent length.

The error is therefore already committed before the first read. The destination object exists, the input allocation has been sized, the source position has been selected, and the caller holds the stream interface. Decompression is still downstream. A safe validation harness can stop here and compare the recorded exponent, returned capacity, requested output cache, and first extent scale without allowing any bytes to enter the undersized region.

Code review follows the actual handoffs through the clusterSizeLog parameter, CompressionUnit field, extent vector, and owned source interface. Patch proof covers two relationships: the 32-bit shift exponent always remains below 32, and every cumulative write into _inBuf stays within recorded capacity.

The object currently remembers buffer pointers and several exponents, but the read call does not receive an explicit input-buffer capacity. That information existed when Alloc() ran and then became implicit. Carrying capacity as a field or span-like view would let the producer enforce its contract locally and would make future changes to geometry less dependent on a distant header check.

Construction also has transactional ownership. CMyComPtr, extent containers, and byte buffers become visible only after parsing, reservation, and seeking all succeed. Any failure leaves *destStream null, clears cache tags, source pointers, and cursors, and makes a retry start clean. Fault injection verifies reference counts, one-time cleanup, and object visibility so a security port does not leave stale interfaces on exceptional paths.

Rejected headers, unsupported geometry, capacity errors, allocation failures, cancellation, and source damage remain distinct internal causes through the archive caller. No path may reuse a partly initialized stream or retry it through an unchecked constructor. Listing, testing, and extraction must each restore file position, cache state, and reference ownership after failure even when the user-facing message is brief.

4 A Shift by 32 Folds a Compression Unit Into One Byte

4.1 The language stops defining the result before the allocator sees it

The vulnerable expression is small enough to overlook in a long parser: (UInt32)1 << (BlockSizeLog + CompressionUnit). Its apparent purpose is straightforward. A compression unit contains 2^CompressionUnit clusters, each cluster contains 2^BlockSizeLog bytes, so adding the exponents and shifting one should yield the required byte capacity.

The mathematical identity is sound only inside the representable domain of the chosen machine type. The C++ shift rule examines the promoted left operand. Here that operand is explicitly UInt32, with 32 value bits in the builds at issue. If the right operand is negative or greater than or equal to the width of the promoted left operand, behavior is undefined. The sum 28 plus 4 lands on the first excluded value.

Undefined behavior is not a guaranteed wrap to zero, one, or any other convenient result. A compiler may emit an instruction whose processor masks the count, may optimize under the assumption that the invalid case never occurs, or may produce a different result after surrounding code changes. The public research records what the tested optimized x86 and x64 builds did: the effective hardware count wrapped at the instruction level and the expression returned one.

That observed value flows directly into UInt32 cuSize and then _inBuf.Alloc(cuSize). There is no later minimum-size correction, multiplication, or capacity check. The input buffer believes its full compression unit occupies one byte. The error has converted a huge logical unit into a tiny physical object without changing any of the extent metadata that will feed it.

Clang's UndefinedBehaviorSanitizer can flag the exponent at the expression itself. The public advisory records the diagnostic at NtfsHandler.cpp:687 in the tested source. That signal is unusually valuable because it points to the first invalid operation, before heap layout or operating-system behavior complicates the symptom. A release crash may occur much later; a UBSan build places the failure at its arithmetic origin.

The CNA maps the issue to both CWE-190 and CWE-787. The first captures unsafe numeric behavior that creates an undersized allocation; the second captures the later write beyond that allocation. Treating them as two consecutive stages improves review coverage. Numeric sanitizers find the broken exponent, while address sanitizers and capacity assertions find the producer-consumer mismatch.

Checking only whether the result is nonzero would not help. One is nonzero and still catastrophically small. Checking only whether allocation succeeds would also invert the signal: the tiny request is exceptionally easy to satisfy. The useful condition is relational: the capacity returned by GetCuSize() must equal the largest compression-unit byte domain permitted by the validated fields, and every subsequent extent read must fit within that capacity at its current offset.

A checked implementation can express the intent without relying on optimizer folklore. It can verify the exponent against std::numeric_limits<UInt32>::digits before shifting, use a wider type with a deliberate maximum, and compare the calculated read end against the allocated buffer. The upstream project chose an earlier format check that also rules out unsupported NTFS geometry. Whichever form a downstream tree uses, the proof must be about types and bounds, not about one machine's current instruction.

4.2 x86 and x64 take different detours after the same one-byte input request

InitAndSeek() performs a second allocation immediately after _inBuf. The output cache holds two decompressed compression units, because kNumCacheChunksLog is one and kNumCacheChunks is a size_t expression equal to two. Its requested size is kNumCacheChunks << _chunkSizeLog. The exponent is still 32, but the left operand now follows the platform width of size_t.

On a 32-bit build, that second shift is also outside the operand width. The public debugger record observed the same masked-count behavior and a two-byte output allocation. Both tiny allocations succeed, initialization completes, and the stream proceeds toward the extent read without a resource barrier. The advisory describes the overflow as unconditionally reached for the tested 32-bit path once the crafted structures are accepted.

On a 64-bit build, shifting the size_t value two by 32 is defined and yields 8,589,934,592 bytes, or 8 GiB. The input calculation remains a 32-bit undefined shift and still returns one in the observed build. This creates a striking pair: one byte requested for compressed input, eight GiB requested for the two-entry decompression cache.

The order of calls determines the visible result. The input buffer is allocated first. The large output allocation follows before any extent read. If the allocator rejects the 8 GiB request, 7-Zip raises its allocation failure and stops; availability is affected, but the input overwrite has not yet happened. If the request succeeds, initialization continues and the same one-byte input object reaches the read path.

“Succeeds” is an environment observation determined by address-space availability, process limits, allocator strategy, page-file policy, memory pressure, security products, and overcommit behavior. The public debugger record confirmed the path on a 64 GiB machine and characterized systems with ample memory as realistic candidates. The investigation uses the actual allocation result and commit state; installed memory is one input to that result.

The difference explains apparently inconsistent telemetry across a fleet. A 32-bit archive worker can crash near a write or indirect call. A 64-bit desktop with tight commit limits can report out of memory. A high-capacity analysis service can reserve the output cache and continue to a heap overwrite. These are branches of one input and one source defect, not three unrelated problems.

Build property_inBuf expression_outBuf expressionObserved gate before the read
x86, 32-bit size_t32-bit shift by 32; observed result 132-bit shift by 32; observed result 2Both tiny allocations complete
x64, 64-bit size_tStill a 32-bit shift; observed result 164-bit 2 << 32; defined result 8 GiBLarge cache must be reserved before reading
Different compiler or optimizationUndefined input expression may behave differently; no fixed collapsed value is portableSource remains unsafe even if the symptom changes

Crash grouping should preserve this branch information. Collect process architecture, executable and module hashes, compiler provenance when known, exception or allocation status, requested sizes, peak commit, page-file state, and the last completed function. Grouping solely by the final exception code can split the same campaign across “out of memory,” “access violation,” and “invalid control transfer” buckets.

A hand-drawn stream splits into a 32-bit lane with two tiny cups and a 64-bit lane with a huge reservoir before both converge on the same undersized input buffer.
Figure 3. The input allocation is governed by a 32-bit shift on both architectures; the platform-width output allocation determines whether x64 stops at resource exhaustion or continues toward the overwrite.

The regression matrix separates architecture, optimization level, and compiler, and annotates each similar-looking expression with its promoted left-operand type, exponent domain, destination type, and downstream use. Tests must not replace UInt32 with an unlimited integer or a 64-bit debugger expression; each row records sizeof(UInt32), sizeof(size_t), optimization settings, and the actual result.

Disassembly or intermediate representation can explain why a tested build produced one and two, but it cannot make undefined source safe. UBSan establishes the invalid shift, ASan observes the out-of-bounds write, and the optimized release shows deployed behavior. All three bind to the same source and fixture and are labeled for the distinct question they answer.

The 8 GiB request describes two cache slots multiplied by the theoretical 4 GiB unit; extracted-file size is a separate quantity. Telemetry distinguishes requested bytes, address-space reservation, committed backing, and resident working set and retains allocator result, process limits, and time of first source read. The request can fail before the resident working set grows.

The fixed-build oracle does not rely on undefined results: exponent 22 must be rejected before GetCuSize() and both allocations, regardless of architecture, compiler, allocator, or available memory. Reaching a shift or allocation means the patch property is absent even if the run exits harmlessly. One byte and eight GiB remain symptoms of known builds, not acceptance criteria.

5 The First Disk Read Arrives Before LZNT1 Can Object

5.1 A cache miss turns reconstructed extents into a write length

When an archive caller reads the extracted file, CInStream::Read() first handles ordinary stream semantics. It returns end of file at the logical size, trims the requested count to remaining initialized data, and emits zeroes beyond InitializedSize. None of those checks describes the internal compression buffer. They protect the caller's destination and the file's logical range.

The method then calculates a cache tag from _virtPos >> _chunkSizeLog. If one of the two output-cache slots already holds that compression unit, it copies the requested slice to the caller and returns. The vulnerable path begins on a cache miss, when the object must assemble the compressed unit from its physical extents.

CompressionUnit determines how many virtual clusters belong to the unit. The code aligns the current virtual cluster down to the unit start, binary-searches the extent list, and scans the range for an empty extent. An empty region within a compression unit signals the compressed representation. A unit with no physical data becomes sparse zeroes; a unit with physical data enters the input-buffer path.

The stream walks each physical run inside the aligned unit. It calculates a source byte offset from the physical cluster and BlockSizeLog, seeks when needed, counts the consecutive clusters, caps the run at the compression-unit end, and converts that count to bytes with compressed = (size_t)numChunks << BlockSizeLog.

For one cluster at exponent 28, compressed is 256 MiB on the relevant platforms. The destination argument is _inBuf + offs, where offs begins at zero and grows by each completed run. ReadStream_FALSE() receives the calculated length. No comparison asks whether offs + compressed is less than the capacity originally passed to _inBuf.Alloc().

The source length and destination capacity were derived from related fields through different expressions. The read length uses a platform-sized size_t shift by 28 and is well defined. The allocation used a 32-bit shift by 32 and produced one in the observed binaries. Both calculations can complete exactly as their generated machine instructions dictate while disagreeing by 268,435,455 bytes.

ReadStream_FALSE() is a completeness wrapper. It repeatedly calls the source stream, advances the destination by the processed count, subtracts that count from the remaining request, and returns success only if the full size is delivered. Its generic contract assumes the caller supplied enough writable memory. The helper has no allocation object or capacity parameter with which to challenge _inBuf + offs.

The generic stream layer and Windows file layer may divide a large request into smaller operations. In the tagged source, StreamUtils.cpp loops according to processed counts, and FileIO.cpp caps Windows file reads before calling ReadFile. The public debugger session observed 64 KiB iterations in its tested release path. The exact subread size can vary by wrapper and platform; the first successful chunk already exceeds a one-byte destination.

Only after every physical run has been copied does the code call Lznt1Dec(). It passes _inBuf and the accumulated offs as compressed input, selects one output-cache slot, and marks the tag. A malformed compressed token could be rejected there, but the heap has already received the extent bytes. Decompressor hardening cannot repair the earlier capacity failure.

This chronology gives a clean review assertion: on every path that writes to _inBuf + offs, the code needs an invariant equivalent to offs <= capacity and compressed <= capacity - offs. Upstream makes the capacity calculation safe by limiting the inputs. Defense in depth can retain an explicit checked-add assertion near the write as well.

5.2 The heap neighborhood turns a size mismatch into control-flow risk

An out-of-bounds write becomes more than a crash when controlled source bytes reach a security-sensitive neighbor. In this case the source is the NTFS image itself: physical extent content is copied without transformation into the undersized buffer. The attacker can choose those bytes within the constraints of the volume structure and delivery path.

The public debugger session inspected an optimized Windows build whose code generation matched the official release for the relevant functions. In that session, the heap placed the CInStream object 304 bytes after _inBuf. The first observed read operation crossed the object and changed its virtual-table pointer. A later stream call then used the corrupted dispatch state. That sequence supports the advisory's potential arbitrary-code-execution conclusion.

The distance is an observation, not a stable ABI promise. CByteBuffer storage and the CInStream object are separate allocations. Heap implementation, allocation history, process architecture, debug or release flags, loaded modules, and security instrumentation can change their order. Some runs will strike unallocated memory, a guard, another buffer, or allocator metadata before reaching an object with an indirect-call target.

Windows ReadFile requires the output range to be writable for the operation. The public advisory notes that an unmapped or guarded page makes the call fail at that boundary and changes exploit reliability and crash location. A successfully copied prefix can still corrupt adjacent committed memory first, and the one-byte capacity violation remains.

Modern mitigations add further branches. Address-space randomization changes locations, control-flow protections may reject some corrupted indirect calls, allocator checks may terminate on damaged metadata, and endpoint products may intercept the crash. These controls influence outcome after the parser has violated its memory contract. The repair must remove the invalid state at the source.

Forensic collection should begin before repeated testing changes the heap. Preserve the original sparse file with both logical and allocated sizes, calculate a cryptographic hash, record its provenance, and copy it using a method that preserves sparse metadata when that property matters. Repeatedly opening the sample in a production process risks new crashes and destroys useful timing context.

Capture Windows Error Reporting events, application logs, minidumps or full dumps where policy allows, exception codes, faulting modules, call stacks, register state, process architecture, memory-commit pressure, and the loaded paths and hashes of 7z.dll or equivalent modules. The same fields should be collected from Linux or macOS workers with their native core and module metadata.

A source-aligned triage query looks for several symptom families around archive processing: undefined-shift reports in GetCuSize(), enormous allocation requests from InitAndSeek(), faults during reads into an NTFS stream buffer, invalid virtual dispatch from a stream object, and child activity following a 7-Zip process. None is universal alone. Their value comes from correlation with handler selection and the input hash.

If the process continued after handling the file, investigate behavior at the same precision used for any suspected code execution. Review child processes, token or credential access, outbound connections, newly written executables, persistence changes, unusual module loads, and security-control tampering. Separate those observations from parser crashes; a memory fault confirms exposure, while post-exploitation evidence establishes compromise.

A hand-drawn tiny input parcel sits beside a stream-object cabinet while a broad controlled data ribbon crosses the parcel and reaches the cabinet's dispatch tab.
Figure 4. The public debugger observation connects the undersized input allocation to a nearby stream object's dispatch state, while the exact heap distance remains build- and run-specific.

The safest laboratory proof stops before a neighboring object is damaged. Recording allocator and source-stream stubs show that 26.00 forms the capacity/length mismatch and abort before the read, while the same structure reaches CHeader::Parse() in 26.01 or 26.02 and is rejected. That pair proves cause and repair without rehearsing a corrupted indirect call, after which the operational question becomes which workflows can select this parser.

ISequentialInStream permits short reads, and ReadStream() advances the target and tries again. A short first read therefore changes crash order without restoring safety. Network backends, filters, and host APIs can choose different chunk sizes, so a safe stub records each processed count and destination offset and asserts that the cumulative end of all successful writes never exceeds capacity.

Dump analysis maps addresses back to allocation roles: _inBuf start and size, source range, nearby heap blocks, the CInStream address, and any changed virtual-call target. Events are deduplicated by logical input hash, parent-container hash, and parser build while retaining each outcome. That avoids treating the public 304-byte distance as universal or fragmenting one image merely because it was renamed or expanded from sparse storage.

Reusable hardening gives stream helpers a writable span carrying pointer and capacity, maintains a checked cumulative offset, and rejects an oversized end before asking any adapter for bytes. A safe replay can likewise emit only a read plan—physical offset, clusters, byte length, destination offset, and cumulative end—without reading extent content. The 26.00 plan exposes the 256 MiB request; the fixed build's header gate prevents the plan from forming. Early geometry rejection remains the primary repair, while capacity-aware transport catches future miscalculations at the producer-consumer boundary.

6 The NTFS Handler Can Find the Image Under Any Filename

6.1 Extension is a hint; the signature at offset three is the lasting identity

At the bottom of NtfsHandler.cpp, 7-Zip registers the format as NTFS with the extension hints ntfs img. The same record supplies a signature made of N, T, F, S, and four spaces, beginning at offset three. The format identifier in the registration table is 0xD9. These details connect the parser to the archive framework's discovery logic.

An extension can move one handler to the front of the queue, but it cannot revoke content recognition. Public validation confirmed 7-Zip's fallback behavior: when the format suggested by the outer name fails, remaining handlers are tried according to signature priority. A file named like another archive, a document, or an extensionless object can still reach NTFS after its first candidate rejects it.

The discovery path explains why blocking .ntfs and .img is only a convenience filter. It catches honest labels and misses adversarial or accidental renaming. MIME declarations have the same weakness when they come from the sender or from a classifier that looks only at the filename. The durable detection feature is the byte signature plus the parsed geometry.

A signature match proves only that NTFS becomes a handler candidate. The trigger chain also requires CHeader::Parse() to accept the remaining boot fields, successful MFT location and parsing, a compatible non-resident compressed stream, reconstructed extents, and a test or extraction operation that reads the stream. File arrival establishes delivery; a joined job, handler, and stream read establishes vulnerable parsing.

That distinction improves incident timelines. Email or download telemetry establishes when the object entered an environment. File-open and handler telemetry establishes when 7-Zip examined it. Test or extraction jobs establish when the compressed stream may have been read. Crash, allocation, or child-process evidence then shows how far the operation progressed. Collapsing those timestamps into “file seen” loses the causal order.

A nested delivery adds another layer. The NTFS image may itself be stored inside a ZIP, 7z, ISO, installer, or package. Extraction of the outer container can place it on disk without invoking the inner NTFS handler. A subsequent recursive scanner, user action, or pipeline stage may open the inner object. Defenders should preserve parent-child archive relationships and the job stage that selected each handler.

Content scanning must avoid reusing the vulnerable parser as its first line of defense. A small independent reader needs only the boot signature, sector-size field, sectors-per-cluster encoding, and enough bounds checks to calculate the cluster exponent. It can flag ClusterSizeLog > 21 before any MFT traversal, allocation based on the claimed geometry, or compressed-stream read.

That lightweight result proves the signature and abnormal geometry and serves as an early triage signal. Image hash, apparent and allocated sizes, provenance, selected handler, and arrival at a non-resident attribute with compression unit four establish later stages separately. Quarantine can control the high-risk input while those facts remain incomplete.

A hand-drawn line of files wearing different outer wrappers passes a signature inspection window, where the same NTFS seal at the fourth byte routes them to one parser doorway.
Figure 5. After filename routing fails, the eight-byte NTFS signature at offset three can still select the same parser through handler fallback.

6.2 The real fleet includes libraries, workers, plugins, and private copies

Windows packages leave a file manager, command-line tools, shell integration, and 7z.dll; the Extra bundle adds standalone components, and copies placed beside scripts or applications escape the central installer's lifecycle. Linux and macOS 7zz, build caches, container layers, upload gateways, security products, desktop applications, and static integrations remain in scope even when neither the process name nor product version mentions 7-Zip.

Inventory has three layers: package data describes what management expects, filesystem and image-layer discovery locate real objects, and module or process telemetry identifies the copy that handled input. Each copy records owner, business function, input source, execution identity, architecture, path, hash, signer, package or container provenance, last load time, and whether testing or extraction runs automatically.

A version string narrows the search. The source invariant in Chapter 1 and the loaded object determine fixed status: Windows captures module path and hash from a representative process, containers inspect both image layer and running instance, and ephemeral workers bind each invocation to a build manifest and artifact digest. A vendor backport also supplies its header ceiling, rejection stage, and boundary-test result; its own product version cannot replace that evidence.

Historical hunting searches both 7-Zip executables and host applications that load the library, then joins them to email, download, browser, removable-media, upload, artifact, and build records. Content-matched NTFS objects enter the query even when their names say ZIP, RAR, 7z, ISO, or unknown. Nested objects retain parent/child hashes, worker, handler choice, and the actual action—written, listed, tested, or extracted.

A content event keeps the eight-byte signature, derived ClusterSizeLog, original and resolved names, parent and object hashes, sparse-file apparent and allocated sizes, user or service identity, command or API, selected handler, and result. The independent prefilter separately reports missing signature, truncated header, bad sector encoding, supported geometry, oversized exponent, or derivation failure so the quarantine reason can be joined to crash telemetry.

The Chapter 1 response map is the sole execution checklist for this scope. An exception names the exact parser and intake, owner, isolation route, capacity, disabled actions, and expiry; its compensating service is fixed and separated from vulnerable credentials and writable storage. A benign recognition corpus covers honest names, renamed and extensionless objects, truncation, coincidental signature bytes, and nested members written but never opened, allowing repeated fallback tests without dangerous geometry.

7 Version 26.01 rejects the hostile geometry before the stream exists

7.1 One changed limit removes the impossible volume from every downstream path

The official 26.00-to-26.01 comparison contains release commit 8c63d71ff886bda90c86db28466287f977374237. The relevant NtfsHandler.cpp hunk has one deletion and one addition: at lines 122–134, immediately after calculating ClusterSizeLog, the condition changes from > 30 to > 21.

ClusterSizeLog = SectorSizeLog + sectorsPerClusterLog;
if (ClusterSizeLog > 21)
  return false;

Rejection occurs during the first boot-sector parse, before NumClusters drives deep traversal, the MFT opens as a stream, data attributes are grouped, extents are reconstructed, or CInStream allocates either buffer. The ceiling therefore dominates physical offsets, record derivation, extent accounting, stream construction, and cache planning; oversized geometry never enters volume state.

The arithmetic margin is direct. With the only supported compressed exponent, four, the largest GetCuSize() exponent becomes 25. The 32-bit shift returns 33,554,432 bytes, or 32 MiB. The two-slot output-cache expression requests 64 MiB. Both counts are defined on 32-bit and 64-bit builds, and the input buffer covers a complete compression unit under the handler's model.

The fix also returns physical offsets, volume size, record calculations, allocated-size alignment, extent totals, and direct seeks to a domain closer to supported NTFS. The current official 7-Zip history explicitly lists CVE-2026-48095 under 26.01. Version 26.02 remained the official current release on July 26, 2026, and its pinned commit retains the same ceiling. Chapter 6 completes deployment proof with the loaded path, hash, and package or build mapping.

7.2 The cross-field arithmetic invariant must survive backports and future formats

The upstream repair establishes a compact invariant: any accepted ClusterSizeLog, combined with any compression unit that IsCompressionUnitSupported() accepts, yields a shift exponent below 32. That statement connects two parser stages and one typed expression. It is the unit a reviewer should preserve when evaluating a backport.

An equivalent backport review starts in CHeader::Parse(), checks both sectors-per-cluster encodings, the addition that forms ClusterSizeLog, and the effective ceiling, then follows every call path to prove that later assignments cannot replace the result. The stream side checks the actual types of the GetCuSize() left operand, _chunkSizeLog, allocator, and output-cache expression and proves that rejection reaches list, test, and extract operations. An object hash binds an artifact; those domain, type, and control-flow properties define an equivalent fix.

Harmless boundary fixtures begin with valid 512- or 4096-byte boot sectors, change only the encoding needed for cluster exponents 20, 21, and 22, and omit a usable MFT or compressed file. With the rest of the header valid, 20 and 21 may reach the next valid stage while 22 must hit the cluster-ceiling branch; probes also show that the MFT never opens, CInStream is never created, and no allocation or read occurs. A mock constructor then crosses accepted cluster values with CompressionUnit zero and four and records exponent, input capacity, output request, and read attempts.

Compatibility needs its own corpus. Include ordinary Windows-created NTFS images with common 4 KiB clusters, modern large-cluster volumes where available, resident and non-resident files, sparse streams, fragmented extents, uncompressed data, LZNT1-compressed data, alternate data streams, and edge-sized files. Security rejection is useful only if supported images continue to list, test, and extract correctly.

Sanitizers add independent alarms. UBSan should remain quiet for every accepted arithmetic combination. ASan should see no write beyond the input and output buffers. Allocation-failure injection should show that partially initialized stream objects unwind cleanly. A stub source that returns short reads should produce S_FALSE without advancing into uninitialized content.

A hand-drawn boot-sector scale approaches a gate set at the supported two-mebibyte mark, with ordinary volumes passing and oversized geometry stopped before MFT and stream rooms.
Figure 6. Version 26.01 rejects oversized geometry before the MFT, stream, or allocation and gives downstream calculations a supported cluster domain.

Future changes automatically trigger re-review. If GetCuSize(), compression-exponent validation, sectors-per-cluster decoding, cache count, allocator, or read-length type changes, recompute the full domain and memory cost. CI enumerates accepted pairs, proves each shift is defined, and verifies that the maximum producer endpoint stays within capacity. The constant 21 is part of the current proof, not a permanent answer after the format evolves.

Production acceptance also measures resource budgets. At the supported ceiling, one compressed input unit can reach 32 MiB and the two-slot output cache can request 64 MiB. A canary starts with normal and header-only fixtures and records loaded hash, handler choice, rejection reason, peak private bytes, allocation failures, duration, queue, retries, and output before traffic expands. Long-lived processes drain before replacement is complete; compatibility or capacity failures route only to another verified fixed pool.

8 Verify Early Rejection and Close the Real Exposure

8.1 Safe validation observes the disagreement without performing the overwrite

Validation has three layers. Pinned-source review confirms that the ceiling dominates every stream entry. A header unit test checks the supported boundary and the next exponent. A recording allocator and read stub reproduce the 26.00 arithmetic relationship and stop when the proposed write end exceeds capacity. These layers answer source property, control flow, and capacity relationship separately without copying outside the declared range.

Every result binds the repository object or source package, compiler and version, optimization flags, architecture, allocator, sanitizer configuration, and output hash. The vulnerable control records raw fields, exponents, operand width, input and output requests, offs, numChunks, and proposed length before CHeader::Parse(), CMftRec::GetStream(), CInStream::InitAndSeek(), and ReadStream_FALSE(). UBSan reports exponent 32; the mock allocator and read stub record one byte and 256 MiB.

The fixed control uses the same valid header context: 22 stops in CHeader::Parse(), 21 may reach the next valid stage, and the MFT, CInStream, allocation, and read probes show the expected divergence. Malformed signatures, invalid sector sizes, truncated headers, and bad record sizes use separate fixtures with an exact rejection branch so unrelated errors cannot manufacture a pass.

A production canary uses harmless inputs only. Ordinary NTFS images are listed, tested, and extracted against expected file hashes; a header-only oversized-geometry sample confirms early rejection. Module loads, crashes, allocation peaks, latency, retries, and quarantine outcomes are observed for a representative business cycle. Each conclusion names its coverage: “rejected” binds the limit branch, while “no historical match” binds the workers, content recognition, and retention window actually queried.

The final validation package retains the source diff or vendor attestation, artifact and loaded hashes, architecture, boundary logs, ordinary corpus, sanitizer results, monitoring window, exceptions, owner, and independent review verdict. If a later application update restores an old parser, these pinned objects and boundary behaviors provide the comparison.

8.2 Closure covers assets, history, and a safe fallback

The Chapter 1 response map already assigns owners, actions, and exit conditions. Closure checks those conditions against real assets. Every parser has an owner, input source, current loaded hash, target build, temporary control, and verification state. Stale binaries, expired exceptions, and temporary queues are removed as their business paths recover.

Historical review separates delivered-only, vulnerable parsing executed, and suspected or confirmed compromise. Content arrival supports the first state. A test or extraction job joined to a vulnerable loaded object supports the second. Independent child-process, file, credential, or network behavior moves the case to the third. Queries connect object and parent container to job, process, loaded module, and later telemetry while stating every missing edge and retention limit.

When post-parser behavior appears, isolate the host or workload, preserve volatile and persistent artifacts, rotate credentials available to the service identity, invalidate tokens, rebuild from trusted sources, and inspect downstream caches and artifacts created by the suspicious job. Replacing the parser changes future execution and cannot undo completed actions.

A compatibility or capacity incident withdraws the affected feature, reduces concurrency, or moves traffic to a pre-verified fixed pool. Error rate, queue age, memory ceiling, and customer-impact thresholds are set before deployment; emergency packages and pools are hashed, signed, and capacity-tested in advance. Automatic fallback excludes 26.00 and every unidentified object.

Closure reports challengeable denominators: known instances, instances observed running in the monitoring window, fixed instances verified by loaded hash, isolated exceptions, and unresolved owners. It also reports module-telemetry coverage, oversized-geometry rejections, unexpected NTFS handler selections, ordinary-job success, allocation failures, retries, and queue age. Execution of an old hash, an expired exception, a new integration without ceiling proof, or monitoring coverage below the agreed floor reopens the work.

A hand-drawn laboratory gate rejects an oversized disk ruler, then a response team inventories parser copies, replaces them, hunts historical jobs, and seals an evidence box.
Figure 7. Closure needs technical and operational proof: the oversized header stops before stream construction, and every reachable parser copy is replaced and historically reviewed.

The final harmless exercise submits both a renamed header-only object and a normal NTFS image, checking the prefilter reason, isolation route, early rejection at 22, expected file hashes, actual loaded module, object-to-process join, and safe fallback. Once complete, parser geometry, allocation capacity, and producer length share one reviewable contract from the first sector to the last read.

Research record

9Evidence, objects, and sources

The material below preserves the identifiers and references used in this report.

9.1Research objects

Products, actors, techniques, affected objects, and control points discussed in the report.

CVECVE-2026-48095

7-Zip NTFS compressed-stream heap buffer overflow caused by input-buffer under-allocation

Affected product7-Zip <= 26.00

GitHub CNA affected range; include embedded and renamed parser copies

First fixed release7-Zip 26.01

Commit 8c63d71ff886bda90c86db28466287f977374237 changes the cluster-log limit

Current verified release7-Zip 26.02

Tag f9d78aff31a5f2521ae7ddbdc97c4a8855808959 retains the fixed bound

Source invariantClusterSizeLog <= 21

The NTFS header must reject unsupported geometry before stream construction

Handler signatureNTFS at offset 3

Content-based fallback can select the handler outside .ntfs and .img names

9.2Event chronology

  1. Private report delivered

    GitHub Security Lab submitted the compressed-stream under-allocation through the project's private SourceForge channel.

  2. 7-Zip 26.01 released

    Release commit 8c63d71ff886bda90c86db28466287f977374237 lowered the accepted cluster logarithm from 30 to 21.

  3. GHSL-2026-140 published

    The research disclosed the arithmetic path, architecture-dependent allocation behavior, debugger observations, handler fallback, and tested impact.

  4. CVE record published

    The GitHub CNA record assigned CVE-2026-48095, a CVSS 3.1 score of 8.8, and affected versions through 26.00.

  5. 7-Zip 26.02 released

    The current public release retained the ClusterSizeLog <= 21 source property.

  6. SOSEC rechecked current release and source boundary

    SOSEC rechecked the official current release, three pinned commits, exact source lines, NTFS documentation, and safe boundary tests.

9.3Sources and material

  1. GitHub Security Lab GHSL-2026-140 technical advisoryhttps://securitylab.github.com/advisories/GHSL-2026-140_7-Zip/
  2. CVE.org record for CVE-2026-48095https://www.cve.org/CVERecord?id=CVE-2026-48095
  3. Vulnerable shift and allocation in pinned 26.00 source, lines 680–697https://github.com/ip7z/7zip/blob/839151eaaad24771892afaae6bac690e31e58384/CPP/7zip/Archive/NtfsHandler.cpp#L680-L697
  4. Fixed header bound in pinned 26.01 source, lines 122–134https://github.com/ip7z/7zip/blob/8c63d71ff886bda90c86db28466287f977374237/CPP/7zip/Archive/NtfsHandler.cpp#L122-L134
  5. Official 7-Zip comparison from 26.00 to 26.01https://github.com/ip7z/7zip/compare/26.00...26.01
  6. 7-Zip 26.01 release and fix commithttps://github.com/ip7z/7zip/commit/8c63d71ff886bda90c86db28466287f977374237
  7. Current pinned 26.02 source retaining the fix, lines 122–134https://github.com/ip7z/7zip/blob/f9d78aff31a5f2521ae7ddbdc97c4a8855808959/CPP/7zip/Archive/NtfsHandler.cpp#L122-L134
  8. Official 7-Zip 26.01 release announcementhttps://sourceforge.net/p/sevenzip/discussion/45797/thread/555e132ba4/
  9. Official 7-Zip version historyhttps://www.7-zip.org/history.txt
  10. Official current 7-Zip download pagehttps://www.7-zip.org/download.html
  11. Complete-read loop in pinned 26.00 source, lines 54–77https://github.com/ip7z/7zip/blob/839151eaaad24771892afaae6bac690e31e58384/CPP/7zip/Common/StreamUtils.cpp#L54-L77
  12. Windows chunked read in pinned 26.00 source, lines 469–489https://github.com/ip7z/7zip/blob/839151eaaad24771892afaae6bac690e31e58384/CPP/Windows/FileIO.cpp#L469-L489
  13. Microsoft NTFS overview and supported cluster sizeshttps://learn.microsoft.com/en-us/windows-server/storage/file-server/ntfs-overview
  14. Microsoft MS-FSA product behavior for NTFS cluster and compression unitshttps://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fsa/4e3695bd-7574-4f24-a223-b4679c065b63
  15. Microsoft FILE_COMPRESSION_INFORMATIONhttps://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_compression_information
  16. C++ working draft rules for shift operatorshttps://eel.is/c++draft/expr.shift
  17. Clang UndefinedBehaviorSanitizer documentationhttps://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
  18. Clang AddressSanitizer documentationhttps://clang.llvm.org/docs/AddressSanitizer.html
  19. Microsoft ReadFile documentationhttps://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
  20. MITRE CWE-787 Out-of-bounds Writehttps://cwe.mitre.org/data/definitions/787.html
  21. MITRE CWE-190 Integer Overflow or Wraparoundhttps://cwe.mitre.org/data/definitions/190.html