CNA-BUG-126: ContentReader::InnerReadObjectAny bypasses the object-nesting depth guard, so nested object-typed values recurse without limit

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.

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

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.

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

Known issues
Bug index