CNA-BUG-125: DecompressXnbPayload grows the LZX output without bound and checks it against the declared size only after the last block

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

The LZX loop appends up to 64 KiB per block into an unbounded MemoryStream, so XnbReadLimits::maxDecompressedSize bounds only the declared size, not the memory the decoder actually allocates.

Identifier
CNA-BUG-125
Category
Bug
Subsystem
Content & XNB/CNB/CNJ
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
CNA::Internal::Xnb::DecompressXnbPayload and XnbReadLimits::maxDecompressedSize

Expected behaviour

XnbReadLimits.hpp documents maxDecompressedSize as the “largest decompressed XNB payload this reader will allocate”. The LZ4 decoder in the same file honours that: it allocates exactly the declared size and fails as soon as a literal or match would exceed it.

Actual behaviour at TARGET

DecompressXnbPayload in XnbDecompression.cpp validates the declared size, then decodes block after block into a growable MemoryStream; each block writes frame_size bytes (up to 0xFFFF) and the loop ends only when the compressed input is consumed. The produced length is compared with the declared size once, after the loop. A crafted file within the 64 MiB maxFileSize can therefore make the decoder allocate far more than 256 MiB before that comparison rejects it.

Source locations

Evidence

Checked by reading at 009d40f5; not executed and no bomb file was built. The achievable ratio depends on how many output frames a crafted LZX stream can encode per input byte; the bound on memory is not measured.

Focused reproduction

No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.

Current tests

LzxDecoderFuzzTests.cpp mutates real LZX payloads and the declared size and asserts that every call completes or fails with a clean exception (also under ASan/UBSan); it does not bound peak allocation, and random mutation rarely yields a valid high-ratio stream.

Regression test

A synthetic LZX stream whose blocks decode to more than the declared size, expecting ContentLoadException before the output exceeds the declared size (the fix is to stop, or fail, when decompressedStream would pass decompressedSize).

Blast radius

Every LZX-compressed .xnb, typed and untyped. Only malformed or hostile files are affected; valid files decode to their declared size.

Workaround

Load content only from trusted sources.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index