CNA-BUG-120: ContentManager::LoadXnbAssetUntyped lacks LoadXnbAsset<T>'s compressed-header guard and refuses LZ4
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
modules/content/src/Xna/ContentManager.cpp— ContentManager::LoadXnbAssetUntyped - no 14-byte guard, LZ4 refusedmodules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::LoadXnbAsset - the typed reference behaviourmodules/content/src/Xna/ContentReader.cpp— ContentReader::ReadExternalReference() - the only caller, via LoadUntypedXnbReferencemodules/content/src/Xnb/EffectMaterialContentTypeReaders.cpp— ExternalReferenceReader::Readmodules/content/src/Xnb/XnbDecompression.cpp— DecompressXnbPayload - rejects the negative size afterwards
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- XNB guide: container format support
- Known issues
- Bug index