CNA-BUG-126: ContentReader::InnerReadObjectAny bypasses the object-nesting depth guard, so nested object-typed values recurse without limit
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.
maxObjectNestingDepth is enforced only by the typed InnerReadObject<T>; Dictionary<String,Object> values, Model tags, shared resources and untyped roots dispatch through InnerReadObjectAny, which never counts depth.
- Identifier
CNA-BUG-126- 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
- ContentReader::ReadObject() (untyped), ContentReader::ReadAsset() (untyped), ContentReader::ReadSharedResources and XnbReadLimits::maxObjectNestingDepth
Expected behaviour
XnbReadLimits.hpp documents maxObjectNestingDepth as the “deepest nested-object graph this reader will follow before rejecting the file”, and the ObjectDepthGuard comment (REMED-CONTENT-006) names unbounded reader recursion a bug.
Actual behaviour at TARGET
InnerReadObject<T> in ContentReader.hpp checks the depth and holds an ObjectDepthGuard. InnerReadObjectAny in ContentReader.cpp reads the reader index and calls ReadUntyped with no check. StringObjectDictionaryReader::Read in EffectMaterialContentTypeReaders.cpp reads each value with the untyped ReadObject(), so a file nesting Dictionary<String, Object> values inside each other recurses through frames that never raise the counter (the typed key read returns before the next level) until the stack overflows.
Source locations
modules/content/include/Microsoft/Xna/Framework/Content/ContentReader.hpp— ContentReader::InnerReadObject<T> (guarded) and ObjectDepthGuard; ReadObject() and ReadAsset() route to InnerReadObjectAnymodules/content/src/Xna/ContentReader.cpp— ContentReader::InnerReadObjectAny (no guard) and ReadSharedResourcesmodules/content/src/Xnb/EffectMaterialContentTypeReaders.cpp— StringObjectDictionaryReader::Read: values read with the untyped ReadObject()modules/content/tests/Microsoft/Xna/Framework/Content/ContentReaderTests.cpp— ObjectNestingDepthExceedingLimitThrowsContentLoadException covers the typed path only
Evidence
Checked by reading at 009d40f5; not executed. The deep dive on content robustness already states this asymmetry “by reading”.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
ContentReaderTests.cpp pins the typed limit (ObjectNestingDepthExceedingLimitThrowsContentLoadException, ObjectNestingDepthAtLimitDoesNotThrow) with a typed recursive fixture; nothing nests untyped values.
Regression test
The same test with a nested Dictionary<String, Object> fixture and a tight limit, expecting ContentLoadException; the fix is to take the guard in InnerReadObjectAny too.
Blast radius
Hostile or corrupted .xnb files using object-typed values: dictionaries of objects, Model/ModelMesh tags, shared resources and untyped external-reference roots. Typed graphs are bounded.
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