CNA-BUG-150: StorageDevice::EnsureStorageRoot publishes its initialised flag before the root, without synchronisation

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.

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

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.

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

Known issues
Bug index