CNA-BUG-150: StorageDevice::EnsureStorageRoot publishes its initialised flag before the root, without synchronisation
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.
The process-wide storage root is latched by a plain bool that is set before the path is computed, so a second thread that touches storage during the first resolution receives an empty root.
- Identifier
CNA-BUG-150- Category
- Bug
- Subsystem
- Storage
- 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
- Microsoft::Xna::Framework::Storage::StorageDevice::GetStorageRootEXT() (“Absolute path to the storage root”) and every storage operation that resolves the root
Expected behaviour
GetStorageRootEXT() and the container operations built on it always see the absolute root. SetAppNameEXT is documented to be called once at startup before storage is used, so renaming concurrently is a caller error, but two threads touching storage for the first time is not prohibited anywhere.
Actual behaviour at TARGET
EnsureStorageRoot tests storageRootInitialized_, sets it to true, then resolves the path and finally assigns storageRoot_. All three statics are unsynchronised. A thread that arrives while another is still resolving sees the flag set and returns the empty storageRoot_; relative paths built on it land in the process working directory. The same flag-first order is why a failed resolution latches an empty root for the rest of the process.
Source locations
modules/storage/src/StorageDevice.cpp— StorageDevice::EnsureStorageRoot (flag set before storageRoot_), SetAppNameEXT, GetStorageRootEXTmodules/storage/include/Microsoft/Xna/Framework/Storage/StorageDevice.hpp— static storageRoot_, appName_, storageRootInitialized_; GetStorageRootEXT and SetAppNameEXT documentation
Evidence
Checked by reading StorageDevice.cpp and its header at 009d40f5. The race is a consequence of the statement order; it was not provoked. Not executed.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
StorageDeviceTests.cpp is single-threaded; no test covers concurrent first access or a failed first resolution.
Regression test
A ThreadSanitizer test that resets the root with SetAppNameEXT and then calls GetStorageRootEXT() from several threads at once, expecting the same non-empty absolute path from every thread; a variant with an unwritable root expecting every call to fail rather than to return an empty path.
Blast radius
Games that save or query storage from more than one thread before the root has been resolved once (for example a background save racing the first storage query of the main thread). Single-threaded storage use, and any use after the first successful resolution, is unaffected.
Workaround
Call StorageDevice::GetStorageRootEXT() once on the main thread at startup, after SetAppNameEXT, before any other thread touches storage.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Storage: the storage root
- Internals
- Storage module internals: the root
- Maintainer workflow
- Case study: storage limits · Thread and callback map
- Known issues
- Bug index