CNA-BUG-151: StorageDevice::EnsureStorageRoot latches an empty root after a failed resolution, and storage then operates in the working directory
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 initialised flag is set before the root is resolved, so after a failed directory creation every call returns an empty root, which the path helpers treat as '.', and containers and DeleteContainer then act in the working directory.
- Identifier
CNA-BUG-151- Category
- Bug
- Subsystem
- Storage
- 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::Storage::StorageDevice::GetStorageRootEXT() (documented as the absolute storage root), SetAppNameEXT, EndOpenContainer, DeleteContainer and the device properties after a failed root resolution
Expected behaviour
GetStorageRootEXT() is documented to return the "Absolute path to the storage root". A root that cannot be created is reported with StorageDeviceNotConnectedException, and a device that cannot reach its storage keeps reporting that instead of silently choosing another location. Above all, DeleteContainer must never delete outside the storage root, which is the authority contract the containment work (REMED-CONTENT-002) established.
Actual behaviour at TARGET
EnsureStorageRoot in StorageDevice.cpp sets storageRootInitialized_ = true before computing the root. When create_directories fails (XDG_DATA_HOME naming a regular file, a read-only home directory, a name Windows cannot create), it throws StorageDeviceNotConnectedException with storageRoot_ still empty, and every later call returns the empty string without retrying. This includes the call inside SetAppNameEXT, which then throws before pointing Sharp Runtime's isolated storage at the new root. The core helpers treat an empty base as . (PathContainment.hpp; PathContainmentTest.EmptyBaseDirTreatsCurrentDirectoryAsBase pins that), so afterwards:
EndOpenContainer("Saves")creates./Saves/AllPlayersin the working directory;DeleteContainer(title)removes./titlerecursively, so a title that matches a working-directory folder (the game's ownContentfolder, for example) deletes that folder;getIsConnectedPropertyreports false and the space properties reportLLONG_MAX, becausefs::exists("")is false;- the gamer-services store, which follows
GetStorageRootEXT(), writes to./GamerServices.
Only another SetAppNameEXT clears the latch.
Source locations
modules/storage/src/StorageDevice.cpp— StorageDevice::EnsureStorageRoot sets the flag before resolving; SetAppNameEXT; DeleteContainer; getIsConnectedProperty and the space propertiesmodules/storage/include/Microsoft/Xna/Framework/Storage/StorageDevice.hpp— GetStorageRootEXT documented as the absolute path to the storage rootmodules/core/include/CNA/Internal/PathContainment.hpp— ResolveContainedPathFromBase and ValidateContainedNativePath treat an empty base or root as the current directorymodules/core/tests/CNA/Internal/PathContainmentTests.cpp— EmptyBaseDirTreatsCurrentDirectoryAsBasemodules/gamer-services/src/Internal/LocalGamerServicesStore.cpp— StoreRoot follows GetStorageRootEXT
Evidence
Checked by reading StorageDevice.cpp, PathContainment.hpp and the storage tests at 009d40f5; not executed, and no storage test makes root creation fail. The same statement order is the root cause of the concurrent-first-use race another package publishes as CNA-BUG-150; this entry is the single-threaded failure path, which needs no second thread. The storage internals page records the latch.
Focused reproduction
# Illustrative (POSIX); not run. XDG_DATA_HOME names a regular file.
touch "$PWD/not-a-directory"
XDG_DATA_HOME="$PWD/not-a-directory" ./my_game// Inside my_game; illustrative, not compiled.
try { StorageDevice::SetAppNameEXT("MyGame"); } // throws: the root cannot be created
catch (const StorageDeviceNotConnectedException&) {}
auto device = StorageDevice::EndShowSelector(
StorageDevice::BeginShowSelector(nullptr, nullptr).get());
auto open = device->BeginOpenContainer("Saves", nullptr, nullptr);
auto saves = device->EndOpenContainer(open.get()); // creates ./Saves/AllPlayers
device->DeleteContainer("Content"); // removes ./Content recursively
Current tests
StorageDeviceTests.cpp always resolves the root successfully; PathContainmentTests.cpp pins the empty-base behaviour that the failure path inherits. Nothing covers a failed first resolution.
Regression test
A test with XDG_DATA_HOME pointing at a regular file that expects SetAppNameEXT, GetStorageRootEXT, EndOpenContainer and DeleteContainer all to throw StorageDeviceNotConnectedException, with a sentinel in the working directory that must survive. The fix is to publish the flag only after storageRoot_ is assigned, or to refuse an empty root at every use.
Blast radius
Hosts where the per-user data root cannot be created (sandboxed or read-only home directories, a broken XDG_DATA_HOME, unusual Windows names): saves and the gamer-services store silently land in the working directory, and DeleteContainer can remove unrelated working-directory folders. Hosts with a creatable root are unaffected.
Workaround
Call StorageDevice::GetStorageRootEXT() once at startup and treat an exception or an empty result as fatal for storage.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Storage guide: the storage root
- Maintainer workflow
- Case study: storage limits
- Known issues
- Bug index