CNA-BUG-119: The XNB type-reader registry is process-wide and unsynchronised, and every reader construction writes to it
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.
Every ContentTypeReader constructor writes the unlocked process-wide TargetTypeNames map during each .xnb load, so two XNB loads on different threads race unless a GL renderer's context lease happens to serialise them; XNA and FNA lock this registry.
- Identifier
CNA-BUG-119- 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
- Microsoft::Xna::Framework::Content::ContentManager::Load<T>() / ReadAsset<T>() for .xnb assets on more than one thread; ContentTypeReaderManager
Expected behaviour
XNA 4.0 serialises its process-wide reader tables (Monitor.Enter in ContentTypeReaderManager) and FNA does the same (lock (locker)), so separate ContentManagers may load on separate threads. CNA intends the same for its own format: the CNB loader registry is guarded by a std::shared_mutex, and CnbHardeningTests states that programs build content managers on more than one thread.
Actual behaviour at TARGET
ContentTypeReaderManager keeps TypeCreators() and TargetTypeNames() as function-local static unordered_maps with no lock. ContentReader::InitializeTypeReaders constructs a fresh reader for every entry of every .xnb file, and each ContentTypeReader<T> constructor writes TargetTypeNames()[typeid(T)] through AssociateTargetType. Two threads loading .xnb files at once therefore write the same map concurrently: undefined behaviour, including a rehash during another thread's insertion. The only incidental serialisation is the renderer-thread context lease a ContentReader takes when its manager has a direct device pointer, which only the GL renderer families return. The per-manager asset cache is unsynchronised too, but that matches XNA's own ContentManager and is not part of this entry.
Source locations
modules/content/src/Xna/ContentTypeReaderManager.cpp— TypeCreators, TargetTypeNames, AssociateTargetType, CreateReader: no lockingmodules/content/include/Microsoft/Xna/Framework/Content/ContentTypeReader.hpp— ContentTypeReader constructors call detail::AssociateReaderTargetTypemodules/content/src/Xna/ContentReader.cpp— ContentReader::InitializeTypeReaders creates readers per file; the constructor takes the renderer-thread lease only with a direct devicemodules/content/include/CNA/Content/Cnb/CnbLoaderRegistry.hpp— the CNB registry's shared_mutex, for comparison
Evidence
Checked by reading ContentTypeReaderManager.cpp, ContentTypeReader.hpp and ContentReader.cpp at 009d40f5; XNA's locking was read from the IL of Microsoft.Xna.Framework.Content.ContentTypeReaderManager and FNA's from its source. The race was not provoked and no ThreadSanitizer run was made.
Focused reproduction
Illustrative, not compiled: two threads, each with its own device-less ContentManager, each loading a different .xnb asset of the same reader type in a loop, under ThreadSanitizer; the expected report is a write/write race inside ContentTypeReaderManager::AssociateTargetType.
Current tests
ContentTypeReaderManagerTests.cpp is single-threaded. CnbHardeningTests.cpp (ConcurrentRegistrationLookupAndContentManagerConstructionAreRaceFree) covers only the CNB registry.
Regression test
The XNB counterpart of the CNB hardening test: several threads constructing managers and loading small .xnb fixtures concurrently, run under ThreadSanitizer, asserting only interleaving-independent outcomes.
Blast radius
Programs that load .xnb content on more than one thread at a time (a loading screen whose worker loads while the main thread also loads, or separate managers on worker threads) on renderer families that return no context lease, or with managers that reach their device only through the service provider. Single-threaded loading, CNB and loose-file loading, and GL renderers with a directly bound device are unaffected.
Workaround
Load .xnb content from one thread at a time (for example behind one application-level mutex around Load).
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Tutorial 77: async loading
- Architecture
- Content architecture
- Internals
- Content runtime internals: three registries, three scopes · Content runtime internals: thread and renderer affinity
- Maintainer workflow
- Thread and callback map: content
- Known issues
- Bug index