CNA-GAP-048: StorageContainer::OpenFile(file, mode, access, share) ignores its FileShare argument

CNA snapshot 009d40f5  ·  Known Issues › Functional gaps  ·  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 four-argument OpenFile accepts a FileShare value and discards it, because the Sharp Runtime FileStream it constructs has no share mode, so FileShare::None does not keep a second writer out.

Identifier
CNA-GAP-048
Category
Functional gap
Subsystem
Storage
Status
Narrowed (partially fixed; describes only what survives)
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
Microsoft::Xna::Framework::Storage::StorageContainer::OpenFile(const std::string& file, FileMode, FileAccess, FileShare), the two shorter OpenFile overloads that forward to it, and the C route cna_storage_container_open_file_share

Expected behaviour

XNA 4.0's OpenFile(file, fileMode, fileAccess, fileShare) hands the share mode to File.Open (decompiled StorageContainer), and CNA's own header describes the parameter as the "Sharing mode for concurrent access". A second open that conflicts with FileShare::None should fail, as it does with a .NET FileStream on Windows.

Actual behaviour at TARGET

In StorageContainer.cpp the parameter is unnamed (/*fileShare*/) and the method constructs System::IO::FileStream(ResolvePath(file), fileMode, fileAccess). The two- and three-argument overloads forward FileShare::ReadWrite into the same discard. The Sharp Runtime FileStream constructor has no share parameter at all, so no share mode reaches the host and there is no cross-process lock: None, Read and ReadWrite behave identically. The C route validates the share bits and passes them to the same overload (CnaCApiStorage.cpp). Neither the header nor the returned stream says that the value was ignored.

The larger defect first reported together with this one is gone: every container path now goes through the containment helpers (see the evidence), so only the share-mode half survives.

Source locations

Evidence

Checked by reading StorageContainer.cpp, its header and CnaCApiStorage.cpp at 009d40f5; not executed. The FileStream signature was read in a sibling Sharp Runtime checkout (next at 41b918c9, not pinned by TARGET). The containment half of the original report is absent at TARGET, with positive evidence: the constructor confines the display name with CNA::Internal::ResolveContainedPath, every path-taking member goes through ResolveNativePath and ResolveContainedNativePathFromBase with the canonical (symlink-resolving) check on, and StorageDeviceTests.cpp pins it with ContainerOpenRejectsPathsOutsideStorageRoot, ContainerOperationsRejectLexicalEscapes, ContainerOperationsRejectSymlinkEscapes and ContainerAllowsNormalizedPathsThatRemainContained. The storage guide already tells games not to rely on FileShare.

Independently observed as a separate finding (merged): The four-argument OpenFile discards fileShare and the underlying FileStream has no share mode, so FileShare::None does not keep other writers out.

Focused reproduction

// Illustrative; not compiled or run for this entry.
using namespace System::IO;
auto first  = container->OpenFile("save.dat", FileMode::OpenOrCreate,
                                  FileAccess::ReadWrite, FileShare::None);
auto second = container->OpenFile("save.dat", FileMode::Open,
                                  FileAccess::ReadWrite, FileShare::None);
// XNA / .NET on Windows: the second call throws IOException (sharing violation).
// CNA: both streams open, and writes through either reach the same file.

Current tests

StorageDeviceTests.cpp has no share-mode case. CApi_StorageSmoke (StorageSmoke.c, C ABI builds only) checks that an unknown share bit is refused and that a valid mask opens a stream; it does not check that the mask has any effect.

Regression test

A test that opens a file with FileShare::None and expects a second conflicting open to fail, on the hosts where the chosen implementation can enforce it, with the POSIX behaviour (advisory locking or none) written down. Until share modes exist, a note on OpenFile that the value is ignored would at least make the gap discoverable from the header.

Blast radius

Games that open the same save file twice (two streams, two containers, or another process such as a launcher) and rely on FileShare::None or FileShare::Read to keep writers apart. Code with a single writer per file is unaffected.

Workaround

Serialise saves in the game: keep at most one open stream per file, or write to a temporary name and rename it.

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

Maintainer workflow
Case study: storage limits