CNA-BUG-122: ContentManager::Load<T> leaks non-XNA exception types: std::bad_any_cast for a wrong .xnb type, unwrapped Load<SoundEffect> errors, std::runtime_error when disposed

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.

ContentManager::Load wraps loose-reader failures in ContentLoadException, but its .xnb tier lets std::bad_any_cast escape, the SoundEffect specialization wraps nothing, and a disposed manager throws std::runtime_error.

Identifier
CNA-BUG-122
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
ContentManager::Load (documented to throw ContentLoadException) and its SoundEffect specialization; XNA ContentManager.Load (ObjectDisposedException, ContentLoadException including BadXnbWrongType)

Expected behaviour

XNA 4.0 throws ObjectDisposedException from Load<T> on a disposed manager, a ContentLoadException (BadXnbWrongType, raised in ContentReader.InvokeReader) when the asset's root object is not a T, and a ContentLoadException for open and read failures. CNA documents Load<T> as throwing ContentLoadException when the asset cannot be loaded, its .cnb tier converts a wrong type into a ContentLoadException naming both types, its loose tier wraps every std::exception, and ReadAsset<T> throws System::ObjectDisposedException.

Actual behaviour at TARGET

  • Wrong type on the .xnb tier. ContentReader::InnerReadObject<T> (ContentReader.hpp) does std::any_cast<T> on the root reader's result and nothing between it and Load<T> catches, so Load<Texture2D> of a sound .xnb throws std::bad_any_cast.
  • Load<SoundEffect> loose tier. The explicit specialization (ContentManager.cpp) returns reader.Read(resolvedPath, *this) without the generic template's try/catch. A missing or undecodable .wav in a sound-enabled build surfaces as System::NotSupportedException ("Failed to load sound: …") from SoundEffect::SoundEffect(const std::string&), which derives from System::Exception, not from ContentLoadException or std::runtime_error.
  • Disposed manager. Load<T>, the SoundEffect specialization and LoadUntypedXnbReference throw std::runtime_error("ContentManager has been disposed.").

XNA-style handlers such as catch (const ContentLoadException&) around an optional or localized asset therefore miss all three.

Source locations

Evidence

Checked by reading at 009d40f5; not executed. CNA's own XnbContainerFuzzTests.cpp counts std::bad_any_cast, std::out_of_range, std::length_error and std::invalid_argument escaping Load<T> on mutated .xnb input as "clean rejections", which confirms that the .xnb tier does not normalize exceptions at all; only the wrong-type case is claimed here as a contract violation. XNA behaviour taken from the decompiled XNA 4.0 ContentManager.Load and ContentReader.InvokeReader. The site already warns about each leak (XNB guide, Tutorial 147, content-runtime internals).

Focused reproduction

Illustrative; not compiled.

ContentManager content(&services, "Content");  // Content/click.xnb holds a SoundEffect
try { auto t = content.Load<Graphics::Texture2D>("click"); }
catch (const ContentLoadException&) { /* not reached: std::bad_any_cast */ }

try { auto s = content.Load<Audio::SoundEffect>("missing"); }  // no missing.* anywhere
catch (const ContentLoadException&) { /* not reached: System::NotSupportedException */ }

content.Dispose();
try { auto t = content.Load<Graphics::Texture2D>("x"); }
catch (const System::ObjectDisposedException&) { /* not reached: std::runtime_error */ }

Current tests

CnjAssetCacheTypeSafetyTests.cpp covers the cache key and the loose tier; ContentManagerTexture2DXnbTests.cpp pins loose-tier wrapping for Texture2D; ContentRuntimeContractTests.cpp checks ObjectDisposedException for ReadAsset<T> only. No test calls Load<T> with the wrong type for an .xnb, Load<SoundEffect> on a missing file, or Load<T> after Dispose().

Regression test

Three EXPECT_THROWs: Load<Texture2D> of tests/assets/xnb/monogame/windows/uncompressed/audio/tone_mono_44khz_16bit.xnb expects ContentLoadException; Load<SoundEffect>("missing") with SDL's dummy audio driver expects ContentLoadException; Load<T> after Dispose() expects System::ObjectDisposedException. Then narrow the accepted list in XnbContainerFuzzTests.

Blast radius

Callers that rely on ContentLoadException for fallback (optional content, localized-asset probing, mod loaders) and code ported from XNA that catches ObjectDisposedException. The .cnb tier and the generic loose tier already conform. A build without SOUND_ENABLED does not throw from SoundEffect's file constructor at all.

Workaround

Catch std::exception around Load<T> and treat any failure as a load failure.

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

Known issues
Bug index