CNA-GAP-048: StorageContainer::OpenFile(file, mode, access, share) ignores its FileShare argument
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
modules/storage/src/StorageContainer.cpp— StorageContainer::OpenFile four-argument overload (the fileShare parameter is unnamed and unused) and the two forwarding overloadsmodules/storage/include/Microsoft/Xna/Framework/Storage/StorageContainer.hpp— OpenFile documentation of fileShare as the sharing mode for concurrent accessmodules/c-api/src/CnaCApiStorage.cpp— cna_storage_container_open_file_share validates the mask and passes it to the C++ overloadmodules/storage/tests/Microsoft/Xna/Framework/Storage/StorageDeviceTests.cpp— container containment tests that pin the fixed half of the original report
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Maintainer workflow
- Case study: storage limits
- Known issues
- Functional gap index