Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Platform-dependent FileStream permissions behavior? #24432

Open
nickwalkmsft opened this issue Dec 12, 2017 · 14 comments
Open

Platform-dependent FileStream permissions behavior? #24432

nickwalkmsft opened this issue Dec 12, 2017 · 14 comments
Labels
area-System.IO enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@nickwalkmsft
Copy link

I might be missing something obvious, but it's strange to me that this throws an IOException on Windows but not on Ubuntu:

var willWork = File.Open(myPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
var willThrowOnlyOnWin = File.Open(myPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);

However, this will throw on both:

var willWork = File.Open(myPath, FileMode.Create, FileAccess.ReadWrite, **FileShare.None**);
var willThrowEverywhere = File.Open(myPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);

Is this expected?

@atykhyy
Copy link

atykhyy commented Jan 27, 2018

The implementation of FileShare in corefx/src/Common/src/CoreLib/System/IO/FileStream.Unix.cs is completely broken and does not fulfil the promises in the documentation. The behavior you observe happens because the Unix implementation uses a flock(2) shared lock for all FileShare values other than FileShare.None. Even worse, it silently ignores attempts to open a file with restrictive sharing options if the underlying OS or filesystem do not support locking; I'm not sure whether there is any sensible way to handle such a situation except throwing something heavy enough to crash the process, but ignoring it is IMHO not defensible because it negates the whole purpose of passing FileShare to FileStream constructors and other methods that use it internally, like FileInfo.Open. The comment in FileStream.Unix.cs justifies this howler by saying that file locks are "only advisory / best-effort", but according to man open(2), the meaning of that "advisory" is that other operations on an open file do not always respect the lock and that you must check it with a fcntl(2), not that the check itself is unreliable. As these locks are used in FileStream, if the lock cannot be taken, the file handle will be immediately closed and an exception thrown, so the "advisory" part of Unix file locks does not come into play.

This issue was discussed earlier in #22011 and closed as "platform limitation".

@stephentoub
Copy link
Member

completely broken

@atykhyy, how do you propose the implementation be improved?

@atykhyy
Copy link

atykhyy commented Jan 27, 2018

If the underlying platform or filesystem does not support locking at all, I'm afraid there is nothing else to do but crash the process, because otherwise the code in it that relies on locking semantics will be running with invalid assumptions, possibly leading to data corruption etc. As for the read/write locks, the situation is bad. It might make marginally more sense to treat FileShare.Write as FileShare.None than to treat FileShare.Read as being the same as FileShare.Write -- at least writers won't conflict with readers -- but it does seem that reproducing Windows file access sharing with Unix primitives is impossible. E.g. as far as I understand (not a Unix pro) all Unix files are always opened with FileShare.Delete.

@stephentoub
Copy link
Member

use fcntl locks

That has worse problems, e.g. from the man page:
"This interface follows the completely stupid semantics of System V and IEEE Std 1003.1-1988
(``POSIX.1'') that require that all locks associated with a file for a given process are removed when
any file descriptor for that file is closed by that process."
You know it's a bad sign when the man page calls the feature "stupid".

@atykhyy
Copy link

atykhyy commented Jan 27, 2018

Sorry, I'd just deleted that. These locks don't provide the necessary granularity either.

@atykhyy
Copy link

atykhyy commented Jan 27, 2018

For what it's worth, Linux has "open file description fcntl locks" which avoid the problem with POSIX fcntl locks, but they have the same single writer-multiple reader semantics.

@atykhyy

This comment has been minimized.

@atykhyy
Copy link

atykhyy commented Jan 30, 2018

On further consideration, it seems to be possible in Linux to imitate Windows file-level locking semantics in cooperating .NET Core programs, building on Linux-specific open file description locks. The key here is that one can lock byte ranges beyond the end of the file. This permits one to define "abstract" ranges to communicate granular lock information; such a scheme can even be made more or less compatible with FileStream.Lock/Unlock if one puts the "abstract" ranges near long.MaxValue and restricts the ranges lockable with FileStream.Lock/Unlock accordingly (some filesystems can theoretically have files larger than long.MaxValue that would have ranges FileStream.Lock/Unlock wouldn't be able to lock anyway). Two "abstract bytes" would be required, call them r and w. One takes a shared lock to use an access and an exclusive lock to try to deny it to subsequent file opens, taking care with lock ordering:

FileAccess FileShare ranges locked
Read ReadWrite r shared
Read Read r shared, then w exclusive
Write Write r exclusive, then w shared
any None rw exclusive
etc.

However, since (a) Unix has no mandatory file locking (Linux sort of does, but it must be turned on on a per-mountpoint and per-file basis and has other problems and limitations; suffice it to say that the official documentation begins with a section titled "Why you should avoid mandatory locking"), and since (b) it is difficult when not impossible (no Unix or Linux file locking mechanism protects against deletion) to provide Windows granularity, passing a FileShare option on Unix does not actually guarantee anything to the caller. Non-.NET processes (or .NET processes using p/invoke, or perhaps just innocently starting a non-.NET process, e.g. an external tool like cc or ffmpeg) can always disregard the lock even if they are aware of it. In such a situation, is it really worth trying to save the pieces? File locks are used to protect against data corruption, and if they can't actually fulfil this function, I believe it is better to admit this upfront than promise protection that will work except when it (silently) doesn't. It's like a wallet that holds your money securely except when it disappears into thin air. The prudent course, therefore, seems to be to deprecate the FileShare enum and all overloads using it from .NET Standard altogether, perhaps along with FileStream.Lock/Unlock which suffer from similar problems. Windows-specific variants of .NET can keep them as a platform-specific extension.

@JeremyKuhne
Copy link
Member

Triage: need to investigate our current usage of advisory locking to see if we address the comments above.

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the Future milestone Jan 31, 2020
chrisd8088 added a commit to chrisd8088/scalar that referenced this issue Sep 5, 2020
The FetchStepCleansUpStaleFetchLock() functional test currently
only succeeds on Windows (and is therefore marked with the
MacTODO.TestNeedsToLockFile category) because it checks whether
the FetchStep command has released its lock file by testing if
the lock file has been deleted.

This works with our WindowsFileBasedLock implementation, which
opens the lock file with FileOptions.DeleteOnClose, so the file is
removed when the lock is released.  However, the MacFileBasedLock
version leaves the file in place and uses flock(2) and LOCK_EX
to acquire and release the lock atomically; there is no simple
way to delete the file while still holding the open file
descriptor needed for flock().

To make this test succeed on Mac/Linux while still being meaningful,
we need to be robust against the lock file existing both at the
start of the test and afterwards, while still ensuring that the
lock has been released by the FetchStep command.

We therefore simply attempt to open the lock file after the fetch
command completes, using the FileShare.None attribute, which will
fail on all platforms if the lock file has not been released.

On Windows this is because WindowsFileBasedLock uses FileShare.Read
to open the lock file, so if the lock is still held our attempt
with FileShare.None will fail.

On macOS (and Linux) this is because our use of flock() in
MacFileBasedLock will conflict with the flock() with LOCK_EX which
the .NET Core Unix implementation of FileShare.None also uses.

See the discussions in microsoft/VFSForGit#213, dotnet/runtime#24432,
as well as the .NET Core library implementation in:

https://github.com/dotnet/runtime/blob/341b8b22280e297daa7d96b4b8a9fa1d40d28
aa9/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs#L73-L8
8

We also rename the test to FetchStepReleasesFetchLockFile() to
more accurately reflect what it is now testing.
chrisd8088 added a commit to chrisd8088/scalar that referenced this issue Sep 5, 2020
The FetchStepCleansUpStaleFetchLock() functional test currently
only succeeds on Windows (and is therefore marked with the
MacTODO.TestNeedsToLockFile category) because it checks whether
the FetchStep command has released its lock file by testing if
the lock file has been deleted.

This works with our WindowsFileBasedLock implementation, which
opens the lock file with FileOptions.DeleteOnClose, so the file is
removed when the lock is released.  However, the MacFileBasedLock
version leaves the file in place and uses flock(2) and LOCK_EX
to acquire and release the lock atomically; there is no simple
way to delete the file while still holding the open file
descriptor needed for flock().

To make this test succeed on Mac/Linux while still being meaningful,
we need to be robust against the lock file existing both at the
start of the test and afterwards, while still ensuring that the
lock has been released by the FetchStep command.

We therefore simply attempt to open the lock file after the fetch
command completes, using the FileShare.None attribute, which will
fail on all platforms if the lock file has not been released.

On Windows this is because WindowsFileBasedLock uses FileShare.Read
to open the lock file, so if the lock is still held our attempt
with FileShare.None will fail.

On macOS (and Linux) this is because our use of flock() in
MacFileBasedLock will conflict with the flock() with LOCK_EX which
the .NET Core Unix implementation of FileShare.None also uses.

See the discussions in microsoft/VFSForGit#213, dotnet/runtime#24432,
as well as the .NET Core library implementation in:

https://github.com/dotnet/runtime/blob/341b8b22280e297daa7d96b4b8a9fa1d40d28
aa9/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs#L73-L8
8

We also rename the test to FetchStepReleasesFetchLockFile() to
more accurately reflect what it is now testing.
chrisd8088 added a commit to chrisd8088/scalar that referenced this issue Sep 5, 2020
The FetchStepCleansUpStaleFetchLock() functional test currently
only succeeds on Windows (and is therefore marked with the
MacTODO.TestNeedsToLockFile category) because it checks whether
the FetchStep command has released its lock file by testing if
the lock file has been deleted.

This works with our WindowsFileBasedLock implementation, which
opens the lock file with FileOptions.DeleteOnClose, so the file is
removed when the lock is released.  However, the MacFileBasedLock
version leaves the file in place and uses flock(2) and LOCK_EX
to acquire and release the lock atomically; there is no simple
way to delete the file while still holding the open file
descriptor needed for flock().

To make this test succeed on Mac/Linux while still being meaningful,
we need to be robust against the lock file existing both at the
start of the test and afterwards, while still ensuring that the
lock has been released by the FetchStep command.

We therefore simply attempt to open the lock file after the fetch
command completes, using the FileShare.None attribute, which will
fail on all platforms if the lock file has not been released.

On Windows this is because WindowsFileBasedLock uses FileShare.Read
to open the lock file, so if the lock is still held our attempt
with FileShare.None will fail.

On macOS (and Linux) this is because our use of flock() in
MacFileBasedLock will conflict with the flock() with LOCK_EX which
the .NET Core Unix implementation of FileShare.None also uses.

See the discussions in microsoft/VFSForGit#213, dotnet/runtime#24432,
as well as the .NET Core library implementation in:

https://github.com/dotnet/runtime/blob/341b8b22280e297daa7d96b4b8a9fa1d40d28
aa9/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs#L73-L8
8

We also rename the test to FetchStepReleasesFetchLockFile() to
more accurately reflect what it is now testing.
chrisd8088 added a commit to chrisd8088/scalar that referenced this issue Sep 5, 2020
The FetchStepCleansUpStaleFetchLock() functional test currently
only succeeds on Windows (and is therefore marked with the
MacTODO.TestNeedsToLockFile category) because it checks whether
the FetchStep command has released its lock file by testing if
the lock file has been deleted.

This works with our WindowsFileBasedLock implementation, which
opens the lock file with FileOptions.DeleteOnClose, so the file is
removed when the lock is released.  However, the MacFileBasedLock
version leaves the file in place and uses flock(2) and LOCK_EX
to acquire and release the lock atomically; there is no simple
way to delete the file while still holding the open file
descriptor needed for flock().

To make this test succeed on Mac/Linux while still being meaningful,
we need to be robust against the lock file existing both at the
start of the test and afterwards, while still ensuring that the
lock has been released by the FetchStep command.

We therefore simply attempt to open the lock file after the fetch
command completes, using the FileShare.None attribute, which will
fail on all platforms if the lock file has not been released.

On Windows this is because WindowsFileBasedLock uses FileShare.Read
to open the lock file, so if the lock is still held our attempt
with FileShare.None will fail.

On macOS (and Linux) this is because our use of flock() in
MacFileBasedLock will conflict with the flock() with LOCK_EX which
the .NET Core Unix implementation of FileShare.None also uses.

See the discussions in microsoft/VFSForGit#213, dotnet/runtime#24432,
as well as the .NET Core library implementation in:

https://github.com/dotnet/runtime/blob/341b8b22280e297daa7d96b4b8a9fa1d40d28aa9/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs#L73-L88

We also rename the test to FetchStepReleasesFetchLockFile() to
more accurately reflect what it is now testing.
@heinrich-ulbricht
Copy link

heinrich-ulbricht commented Jun 16, 2022

Cross-platform file system feature parity seems to be a nightmare to achieve. After 49604 this is the second feature I'd need in a project that does not exist (yet) :/

If anybody knows about a .NET cross-platform file-based locking implementation - I'm all ears :)

(Here's something older, also talking about flock, advisory locking etc. -> https://github.com/sjmelia/flock)

Edit: Every dive into cross-platform dev topics involving Linux for me seems to end up being a sad history lesson. Citing http://0pointer.de/blog/projects/locking.html:

The Disappointing Summary
File locking on Linux is just broken. The broken semantics of POSIX locking show that the designers of this API apparently never have tried to actually use it in real software. It smells a lot like an interface that kernel people thought makes sense but in reality doesn't when you try to use it from userspace.

Here's a list of places where you shouldn't use file locking due to the problems shown above: If you want to lock a file in $HOME, forget about it as $HOME might be NFS and locks generally are not reliable there. The same applies to every other file system that might be shared across the network. If the file you want to lock is accessible to more than your own user (i.e. an access mode > 0700), forget about locking, it would allow others to block your application indefinitely. If your program is non-trivial or threaded or uses a framework such as Gtk+ or Qt or any of the module-based APIs such as NSS, PAM, ... forget about about POSIX locking. If you care about portability, don't use file locking.

Or to turn this around, the only case where it is kind of safe to use file locking is in trivial applications where portability is not key and by using BSD locking on a file system where you can rely that it is local and on files inaccessible to others. Of course, that doesn't leave much, except for private files in /tmp for trivial user applications.

Or in one sentence: in its current state Linux file locking is unusable.

And that is a shame.

Edit2: https://www.samba.org/samba/news/articles/low_point/tale_two_stds_os2.html 😱😱😱
Edit3: https://sqlite.org/lockingv3.html#how_to_corrupt - "How to corrupt your database files" 😔

@adamsitnik
Copy link
Member

If anybody knows about a .NET cross-platform file-based locking implementation

I am afraid that such an implementation does not exist as of today.

Unix simply offers lower granularity that Windows: it offers an exclusive lock and shared lock, which is translates to "nobody can acquire an exclusive lock while I am holding a shared lock". It does not offer the possibility to say: others can read, but not write to the file.

Mono uses in-memory implementation that stores the list of the files and their locks, but it works only for locks within the same process.

What works well everywhere is FileShare.None (the exclusive access).

@kmcclellan
Copy link

Spent a few hours looking into why FileShare.Read wasn't working for my app running on ubuntu. The shared lock it uses doesn't prevent multiple processes opening it for writing. Ideally this would throw an exception on unsupported platforms, instead of ignoring the instruction. At the very least, I would hope to see something in the documentation.

Switching to FileShare.None works OK (but doesn't allow simultaneous reading).

@joshudson
Copy link
Contributor

I got here looking for this for a very different reason. I find myself planning a VFS layer to simulate chroot() for some individual threads (not processes). I find myself now wondering which would be the better case; the dumb thing the APIs currently do or the less dumb thing proposed above so that cooperating process behave as expected.

@joshudson
Copy link
Contributor

@atykhyy That doesn't actually work. I spent some time trying to fill out rest of the table only to discover it's broken.

Suppose we have two processes trying to open the file with FileAccess = Read and FileShare = Read.

  1. The first one takes R shared
  2. The first one takes W exclusive
  3. The second one takes R shared
  4. The second one fails to take W exclusive
    If it assumes the first one is holding the W lock for it, what happens when the lock is released?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.IO enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

No branches or pull requests

9 participants