CNA-BUG-124: ContentManager::Load<T> holds a plain reference to the loose-file reader, so RegisterTypeReader<T> during that reader's Read destroys it mid-call
Evidence basis: source-verified at the pinned commit. 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> binds a LooseFileContentTypeReader<T>& to the object owned by the shared_ptr in typeReaders_; registering another reader for the same T while that Read runs releases the only owner.
- Identifier
CNA-BUG-124- 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
- None
- Affected contract
- ContentManager::RegisterTypeReader<T>(std::unique_ptr<LooseFileContentTypeReader<T>>) and ContentManager::Load<T>
Expected behaviour
RegisterTypeReader<T> is documented without any restriction on when it may be called. A registration that happens while a load of the same type is running should either keep the running reader alive or be refused.
Actual behaviour at TARGET
In ContentManager.hpp, Load<T> takes readerPtr into the std::any stored in typeReaders_, binds LooseFileContentTypeReader<T>& reader = **readerPtr; and calls reader.Read(resolvedPath, *this). RegisterTypeReader<T> assigns a new shared_ptr to typeReaders_[typeid(T)]. If the running Read registers a reader for the same T, directly or through a nested Load of another asset, the map entry was the only owner, so the executing reader is destroyed and the rest of its Read runs on freed memory. RegisterCnjLoader<T> cannot trigger it (it registers only when T has no reader yet), so only application code can.
Source locations
modules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::Load<T>: LooseFileContentTypeReader<T>& reader = **readerPtr, then reader.Readmodules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::RegisterTypeReader<T>: replaces the shared_ptr in typeReaders_
Evidence
Checked by reading at 009d40f5; not executed. No CNA reader registers during a load; the trigger is application code.
Focused reproduction
// Illustrative; not compiled or run.
struct SelfReplacingReader : LooseFileContentTypeReader<MyAsset>
{
MyAsset Read(const std::string& path, ContentManager& cm) override
{
cm.RegisterTypeReader<MyAsset>(std::make_unique<OtherReader>()); // frees *this
return MyAsset{path}; // use after free
}
};
Current tests
No test in modules/content/tests registers a reader from inside a reader.
Regression test
A reader that re-registers its own T during Read, run under AddressSanitizer. The fix is one line: copy the shared_ptr (auto keepAlive = *readerPtr;) before calling Read.
Blast radius
Only applications that register loose-file readers from inside another reader's Read. Built-in readers, the .xnb and .cnb tiers and ordinary registration before loading are unaffected.
Workaround
Register every reader before the first Load.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- ContentManager: custom readers
- Internals
- Content runtime: Load
- Known issues
- Bug index