CNA-BUG-123: ContentManager has no in-progress guard, so an .xnb external-reference cycle recurses until the stack overflows
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.
Load<T> and LoadUntypedXnbReference cache an asset only after it has loaded, so an asset whose external reference leads back to itself re-enters the loader without bound and crashes instead of throwing ContentLoadException.
- Identifier
CNA-BUG-123- 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
- ContentManager::Load<T>(const std::string&), ContentManager::LoadUntypedXnbReference, ContentReader::ReadExternalReference<T>() and ContentReader::ReadExternalReference()
Expected behaviour
CNA's content hardening refuses hostile file structure with a ContentLoadException instead of crashing: XnbReadLimits.hpp exists “to fail fast with a clear error”, and the ObjectDepthGuard comment in ContentReader.hpp calls unbounded recursion “driven entirely by attacker-controlled file structure” a bug class. A reference cycle between assets is the cross-file form of that recursion.
Actual behaviour at TARGET
Load<T> in ContentManager.hpp looks the key up in loadedAssets_ and stores the result only after LoadXnbAsset<T> returns; LoadUntypedXnbReference in ContentManager.cpp does the same under its std::any key. Nothing marks an asset as being loaded. ReadExternalReference<T>() calls Load<T> and the untyped overload calls LoadUntypedXnbReference (ContentReader.cpp), so an EffectMaterial whose effect reference names its own asset, or an ExternalReference value naming an asset that refers back, re-enters the loader, re-reads and re-parses the file each time, and recurses until the stack is exhausted. The per-reader ObjectDepthGuard cannot stop it: each level is a fresh ContentReader starting at depth 0.
Source locations
modules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::Load<T>: cache lookup, cache insert only after a successful loadmodules/content/src/Xna/ContentManager.cpp— ContentManager::LoadUntypedXnbReference: same shape under the std::any keymodules/content/src/Xna/ContentReader.cpp— ContentReader::ReadExternalReference<T>() and ReadExternalReference(): re-enter the managermodules/content/src/Xnb/EffectMaterialContentTypeReaders.cpp— EffectMaterialReader::Read (effect external reference) and ExternalReferenceReader::Read
Evidence
Checked by reading at 009d40f5; not executed. The XNA 4.0 reference behaves the same way: the IL of ContentManager.Load<T> checks loadedAssets and adds only after ReadAsset<T> returns, so genuine XNA would also overflow; its content pipeline cannot emit such a cycle, so only hand-edited, corrupted or hostile files reach it. It is listed as a bug because CNA's own robustness contract promises a clean refusal for hostile structure, not because XNA differs.
Focused reproduction
// Illustrative; not compiled or run. materialA.xnb is an EffectMaterial whose effect
// external reference is "materialB"; materialB.xnb references "materialA".
auto effect = content.Load<std::shared_ptr<Effect>>("materialA");
// Expected: ContentLoadException. Actual: A -> B -> A ... until the stack overflows.
Current tests
No test in modules/content/tests loads a reference cycle. ContentReaderExternalReferenceTests.cpp covers containment (absolute and escaping references are refused). XnbContainerFuzzTests.cpp mutates single files and cannot form a cross-file cycle.
Regression test
Two fixture files that reference each other, plus one that references itself, each expected to throw ContentLoadException. The fix is an in-progress set keyed like the cache (or a load-depth counter on the manager) consulted by both Load<T> and LoadUntypedXnbReference.
Blast radius
Only content with a reference cycle. Pipeline-built content, acyclic reference graphs and the .cnj/.cnb tiers are unaffected. The failure is a process crash, not an exception.
Workaround
Load content only from trusted sources.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- ContentManager: compiled content
- Internals
- Content runtime: the XNB tier
- Deep dives
- Content robustness: recursion
- Known issues
- Bug index