CNA-BUG-123: ContentManager has no in-progress guard, so an .xnb external-reference cycle recurses until the stack overflows

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.

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

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.

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

Known issues
Bug index