CNA-BUG-120: ContentManager::LoadXnbAssetUntyped lacks LoadXnbAsset<T>'s compressed-header guard and refuses LZ4

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 untyped decoder behind EffectMaterial external references reads the 4-byte size at offset 10 without the 14-byte check, over-reading a 10-13-byte compressed file, and refuses LZ4 that the typed load path decodes.

Identifier
CNA-BUG-120
Category
Bug
Subsystem
Content & XNB/CNB/CNJ
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Medium (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
ContentReader::ReadExternalReference() (untyped, CNAEXT; @throws ContentLoadException if the referenced compiled asset cannot be loaded) via ContentManager::LoadUntypedXnbReference / LoadXnbAssetUntyped

Expected behaviour

The untyped decoder validates and decodes exactly like LoadXnbAsset<T> (ContentManager.hpp): a compressed file whose totalLength is below 14 is refused with ContentLoadException before the size field is read, and MonoGame LZ4 payloads are decoded with DecompressXnbLz4Payload.

Actual behaviour at TARGET

LoadXnbAssetUntyped (ContentManager.cpp) checks only that totalLength lies in [10, file size]. In the LZX branch it builds MemoryStream(bytes.data() + 10, 4), and sharp-runtime's pointer constructor copies that range, so for a 10-13-byte file it reads up to four bytes past the end of the vector (a heap over-read, undefined behaviour); the negative compressedSize it then computes is rejected by DecompressXnbPayload. The LZ4 branch throws ContentLoadException "uses MonoGame's Lz4 compression, which CNA does not yet support", while the typed decoder has supported it since DecompressXnbLz4Payload landed. So an EffectMaterial parameter that references an LZ4 texture fails, although Load<Texture2D> of the same file succeeds.

Source locations

Evidence

Checked by reading at 009d40f5; not executed. Compiling ContentManager.cpp alone is impractical, so the over-read was not demonstrated under a sanitizer. The copy semantics of MemoryStream(const bytecs*, intcs) were read in sharp-runtime's next branch at 41b918c9; the exact sharp-runtime revision a TARGET build fetches was not separately pinned. The site's content-runtime internals page records both points by reading.

Focused reproduction

Illustrative; not compiled.

// Content/params.xnb: an EffectMaterial / Dictionary<string, object> whose value is an
// external reference to "tex"; Content/tex.xnb: 12 bytes, 'X' 'N' 'B' 'w' 5 0x80, totalLength 12.
auto values = content.Load<std::map<std::string, std::any>>("params");
// reads bytes 10..13 of a 12-byte buffer before DecompressXnbPayload refuses the size

// With Content/tex.xnb = tests/assets/xnb/monogame/windows/lz4/white-1.xnb instead:
// ContentLoadException "... Lz4 compression, which CNA does not yet support",
// although content.Load<Graphics::Texture2D>("tex") decodes it.

Current tests

EffectMaterialContentTypeReaderTests.cpp (ExternalReferenceReaderPreservesReferencedTextureCubeConcreteType) is the only untyped-reference test, and it uses an uncompressed fixture. ContentReaderExternalReferenceTests.cpp covers the typed route only.

Regression test

Two cases beside the existing one: the same dictionary referencing the LZ4 fixture tests/assets/xnb/monogame/windows/lz4/white-1.xnb must yield a Texture2D, and a reference to a 12-byte compressed-flag file must throw ContentLoadException naming the truncation (run under ASan to catch the over-read). Sharing one header-validation helper between the typed and untyped decoders would make them agree by construction.

Blast radius

Only untyped external references, which today means ExternalReferenceReader inside EffectMaterial parameter dictionaries. Typed references (ReadExternalReference<T>) and direct Load<T> calls use LoadXnbAsset<T> and are unaffected.

Workaround

Keep textures referenced by EffectMaterial content uncompressed or LZX-compressed.

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

Known issues
Bug index