Repository navigation
File writes to Windows NFS share silently fail... #44546
Description
AB#1285380
We are using .NET Core 3.1 on Linux.
When we write to a file on a Windows NFS server, writes silently fail.
The following program illustrates the behavior:
using System;
using System.IO;
namespace test
{
class Program
{
static void Main(string[] args)
{
if( args.Length != 1 )
{
Console.WriteLine("Usage: test <folder>\n");
return;
}
var filename = Path.Combine(args[0], "file.txt");
File.WriteAllText(filename, $"Hello World as of {DateTime.Now}");
Console.WriteLine($"File Content: '{File.ReadAllText(filename)}'");
}
}
}The folder should be a path to a directory on a Windows NFS server.
Basically, what is happening is .net is asking for a shared advisory lock on the file handle, and then issuing a write on that handle.
While that may work against a Linux NFS server, it does not work against a Windows NFS Server, which chooses to enforce advisory locking.
The write into the local buffer cache succeeds, but the write to the back-end server fails.
.net also fails to look at the return value of close(2), and hence never notices anything wrong -- the program above succeeds and shows the file as having no data in it after the write to the file succeeds.
The following native C program exhibits the same issue, for comparison:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/file.h>
int
main(int argc, char **argv)
{
int fd;
int rv;
char buffer[22];
char *filename;
if (argc != 2) {
printf("usage doit <file>\n");
exit(1);
}
filename = argv[1];
fd = openat(AT_FDCWD, filename, O_WRONLY|O_CREAT|O_CLOEXEC, 0666);
if (fd == -1) {
printf("openat(%s) failed %d\n", filename, errno);
exit(1);
}
rv = flock(fd, LOCK_SH|LOCK_NB);
if (rv == -1) {
printf("flock(%s) failed %d\n", filename, errno);
exit(1);
}
memset(buffer, 'a', sizeof(buffer));
rv = write(fd, buffer, sizeof(buffer));
if (rv != sizeof(buffer)) {
printf("write(%s, %d) failed %d, %d\n", filename, (int)sizeof(buffer), rv, errno);
exit(1);
}
printf("write(%s, %d) succeeded %d\n", filename, (int)sizeof(buffer), rv);
rv = close(fd);
if (rv == -1) {
printf("close(%s) failed %d\n", filename, errno);
exit(1);
}
return 0;
}Workarounds for the user include:
- mount the NFS share with "-o nolock" to not propagate lock requests from the Windows NFS Server to Windows NLM.
This is what we are seeing inside the Linux VM, using strace:
openat(AT_FDCWD, "/home/hcsuser/mnt/14/second.txt", O_WRONLY|O_CREAT|O_CLOEXEC, 0666) = 20
fstat(20, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
flock(20, LOCK_SH|LOCK_NB) = 0
fadvise64(20, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
ftruncate(20, 0) = 0
lseek(20, 0, SEEK_CUR) = 0
write(20, "example text to write\n", 22) = 22
flock(20, LOCK_UN) = 0
close(20)
Notice it seems (Linux) .net itself is sending the flock(SH), and according to strace the write succeeds with 22 bytes written.
(The write will not succeed inside Windows NFS Server, because it is incompatible with the lock mode!)
This is what we are seeing on the server (for a different run, but the same outcome):
11:32:59.3285886 AM System 4 4456 IRP_MJ_CREATE N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt NAME NOT FOUND Desired Access: Maximum Allowed, Disposition: Open, Options: Write Through, Open Reparse Point, Attributes: N, ShareMode: Read, Write, Delete, AllocationSize: n/a
11:32:59.3287878 AM System 4 4456 IRP_MJ_CREATE N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt NAME NOT FOUND Desired Access: Maximum Allowed, Disposition: Open, Options: Write Through, Open Reparse Point, Attributes: N, ShareMode: Read, Write, Delete, AllocationSize: n/a
11:32:59.3297571 AM System 4 4456 IRP_MJ_CREATE N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Desired Access: Maximum Allowed, Disposition: Create, Options: Write Through, Non-Directory File, Attributes: N, ShareMode: Read, Write, Delete, AllocationSize: 0, OpenResult: Created
11:32:59.3303417 AM System 4 4456 IRP_MN_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryIdInformation
11:32:59.3303721 AM System 4 4456 IRP_MJ_QUERY_SECURITY N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Information: Owner, Group, DACL, SACL
11:32:59.3303847 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryStandardInformationFile, AllocationSize: 0, EndOfFile: 0, NumberOfLinks: 1, DeletePending: False, Directory: False
11:32:59.3304132 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryBasicInformationFile, CreationTime: 11/10/2020 11:32:59 AM, LastAccessTime: 11/10/2020 11:32:59 AM, LastWriteTime: 11/10/2020 11:32:59 AM, ChangeTime: 11/10/2020 11:32:59 AM, FileAttributes: A 0x400000
11:32:59.3304254 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryStandardInformationFile, AllocationSize: 0, EndOfFile: 0, NumberOfLinks: 1, DeletePending: False, Directory: False
11:32:59.3304437 AM System 4 4456 IRP_MJ_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryFileInternalInformationFile, IndexNumber: 0x3000000000002791
11:32:59.3309122 AM System 4 4456 IRP_MJ_CREATE N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Desired Access: Maximum Allowed, Disposition: Open, Options: Write Through, Sequential Access, Non-Directory File, Open By ID, Open Reparse Point, Attributes: N, ShareMode: Read, Write, Delete, AllocationSize: n/a, OpenResult: Opened
11:32:59.3310229 AM System 4 4456 IRP_MN_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryIdInformation
11:32:59.3310509 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryBasicInformationFile, CreationTime: 11/10/2020 11:32:59 AM, LastAccessTime: 11/10/2020 11:32:59 AM, LastWriteTime: 11/10/2020 11:32:59 AM, ChangeTime: 11/10/2020 11:32:59 AM, FileAttributes: A 0x400000
11:32:59.3310618 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryStandardInformationFile, AllocationSize: 0, EndOfFile: 0, NumberOfLinks: 1, DeletePending: False, Directory: False
11:32:59.3310810 AM System 4 4456 IRP_MJ_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryFileInternalInformationFile, IndexNumber: 0x3000000000002791
11:32:59.3310970 AM System 4 4456 IRP_MJ_QUERY_SECURITY N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Information: Owner, Group, DACL, SACL
11:32:59.3311156 AM System 4 4456 IRP_MN_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryLinks
11:32:59.3311448 AM System 4 4456 FASTIO_LOCK N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Exclusive: False, Offset: 0, Length: 4,294,967,295, Fail Immediately: True
11:32:59.3326908 AM System 4 4456 IRP_MJ_CLEANUP N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS
11:32:59.3329113 AM System 4 4456 IRP_MJ_SET_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: SetBasicInformationFile, CreationTime: 0, LastAccessTime: 0, LastWriteTime: 11/10/2020 11:32:59 AM, ChangeTime: 0, FileAttributes: n/a
11:32:59.3331090 AM System 4 4456 IRP_MJ_SET_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: SetBasicInformationFile, CreationTime: 0, LastAccessTime: 0, LastWriteTime: 0, ChangeTime: 0, FileAttributes: n/a
11:32:59.3331472 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryBasicInformationFile, CreationTime: 11/10/2020 11:32:59 AM, LastAccessTime: 11/10/2020 11:32:59 AM, LastWriteTime: 11/10/2020 11:32:59 AM, ChangeTime: 11/10/2020 11:32:59 AM, FileAttributes: A 0x400000
11:32:59.3331622 AM System 4 4456 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryStandardInformationFile, AllocationSize: 0, EndOfFile: 0, NumberOfLinks: 1, DeletePending: False, Directory: False
11:32:59.3331922 AM System 4 4456 IRP_MJ_QUERY_SECURITY N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Information: Owner, Group, DACL, SACL
11:32:59.3335004 AM System 4 4456 IRP_MJ_WRITE N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt FILE LOCK CONFLICT Offset: 0, Length: 22, I/O Flags: Write Through
11:32:59.3335631 AM System 4 25472 IRP_MJ_SET_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: SetBasicInformationFile, CreationTime: 0, LastAccessTime: 0, LastWriteTime: 0, ChangeTime: 0, FileAttributes: n/a
11:32:59.3336067 AM System 4 25472 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryBasicInformationFile, CreationTime: 11/10/2020 11:32:59 AM, LastAccessTime: 11/10/2020 11:32:59 AM, LastWriteTime: 11/10/2020 11:32:59 AM, ChangeTime: 11/10/2020 11:32:59 AM, FileAttributes: A 0x400000
11:32:59.3336425 AM System 4 25472 FASTIO_QUERY_INFORMATION N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Type: QueryStandardInformationFile, AllocationSize: 0, EndOfFile: 0, NumberOfLinks: 1, DeletePending: False, Directory: False
11:32:59.3336615 AM System 4 25472 IRP_MJ_QUERY_SECURITY N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Information: Owner, Group, DACL, SACL
11:32:59.3339400 AM System 4 4456 FASTIO_UNLOCK_SINGLE N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS Offset: 0, Length: 4,294,967,295
11:32:59.3339829 AM System 4 4456 IRP_MJ_CLEANUP N:\hcsshares\03c059d4-eb0f-4b77-bcb8-14a200969455\datadata\threex\second.txt SUCCESS
Note two potential problems:
- the NFS flock(SH) is mapped to FASTIO_LOCK(Exclusive: False) -- this is unfortunate, since Windows will now fail writes; whereas, NFS would not -- flock(SH) is never appropriate for a handle where writes may occur (.net issues this incorrectly)!
- the write that failed on the server (flushing from Linux buffer cache) is reported as succeeded on the client (write(2) into the buffer cache, and .net did not check the return value of close(2)) -- there is no data in the file.
For the Windows Server lock semantics, see:
Locking a portion of a file for shared access denies all processes write access to the specified region of the file, including the process that first locks the region. All processes can read the locked region.
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
Recommended fix:
- don't ask for lock(SH) on a handle that can be used for write, since if you do, writes will always fail -- the decision to use SH needs more information in .net than it has today (it needs share mode and access rights).
- check the return value for close(2) and throw an exception when it fails -- this will typically be in a caller's using block, if they are trying to control handle lifetimes, which is good..
Activity
danmoseley commentedon Nov 11, 2020
For (2), do you have 3.1.9? I am wondering whether it is fixed by dotnet/coreclr#28099 dotnet/corefx#42988
danmoseley commentedon Nov 11, 2020
I edited your comment to add some triple back tick blocks for formatting
rtestardi commentedon Nov 11, 2020
Thanks! To be clear, I think there are two separate issues here:
rtestardi commentedon Nov 11, 2020
Steve and I confirm the bug (both bugs) still exists in 3.1.10.
PS also to be clear, to repro the issue you want to not use the "nolock" NFS mount option -- in fact, we are using that option as a temporary workaround, which prevents the flock(SH) from reaching the Windows Server device stack, which prevents the write from failing altogether.
jozkee commentedon Feb 16, 2021
Just as an update, I tried to repro the issue using WinNFSd on a Windows Pro machine and then mounting to the nfs server with WSL2 Ubuntu… I had to use NFSv3 since v4 is not supported on WinNFSd.
However I wasn't able to repro the issue with said config; I ran the provided code on the linux client and it successfully writes to the file in the nfs server.
This is how I am mounting it:
My .NET version is 3.1.405.
I wonder if this issue is exclusive to NFSv4 and/or Windows Server's official NFS support.
I'll keep digging.
jozkee commentedon Feb 22, 2021
I was able to repro the issue on a Linux client that @rtestardi gave me access to. I was also able to find a workaround to using
File.WriteAllText:@rtestardi instead of using WriteAllText, which by default requests a shared lock, can you please try to create the FileStream yourself requiring an exclusive lock in the .ctor and then write to it? Use below snippet to replace the code that you attached to this thread:
danmoseley commentedon Feb 22, 2021
Pasting in offline notes:
For the second problem, it sounds like #28099 which definitely went out in 3.1.9 (and in 5.0). You reported the problem on 3.1.10, so it needs further investigation. Based on the description, that should be fixed if it’s not.
For the first problem, to summarize my understanding: StreamWriter (which File.WriteAllText uses) defaults to FileShare.Read (as does FileStream). This has been the case a long time, certainly back in .NET Framework, so it was designed for Windows API, and in Windows I guess it’s fairly typical to allow FileShare.Read (ie FILE_SHARE_READ) when you plan to write (FileAccess.Write, GENERIC_WRITE). Later when FileStream was ported to Unix, where locking is only advisory, we figured FILE_SHARE_READ mapped best to LOCK_SH:
corefx/FileStream.Unix.cs at f168bc36b744e630d89a34b7c90b603458e0e679 · dotnet/corefx (github.com)
// Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive
// lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory,
// and not atomic with file opening, it's better than nothing.
Interop.Sys.LockOperations lockOperation = (share == FileShare.None) ? Interop.Sys.LockOperations.LOCK_EX : Interop.Sys.LockOperations.LOCK_SH;
if (Interop.Sys.FLock(_fileHandle, lockOperation | Interop.Sys.LockOperations.LOCK_NB) < 0)
which would be fine except that you report that the Windows NFS server implementation rejects writes under LOCK_SH.
I’m guessing if we changed FileStream to map FileShare.Read to LOCK_EX it might be quite breaking at this point – any thoughts?
Do I understand correctly that you believe Linux NFS servers allow writes under LOCK_SH – if so, is Windows intentionally behaving differently? Wouldn’t that imply any client of a Linux NFS server could break against a Windows NFS server?
Is there an easy way (eg docker container) to set up such a Windows NFS server?
jozkee commentedon Feb 22, 2021
Agree; I don't think that's something that we should change if this the only backing scenario that proves that FIleShare.Read = LOCK_EX. IMO it would require more investigation. cc. @carlossanlop @adamsitnik.
rtestardi commentedon Feb 22, 2021
rtestardi commentedon Feb 22, 2021
18 remaining items
stephentoub commentedon Mar 30, 2021
As I noted earlier, the more important scenario is folks creating FileShare.None FileStreams, expecting other attempts to create FileStreams to fail. If we, for example, delete the FileShare-related code, such concurrency prevention will simply silently go away.
I outlined all of the options I'm aware of in #44546 (comment).
rtestardi commentedon Mar 30, 2021
I'd hardly say returning an error on concurrent access is "more important" than not silently corrupting data for most basic non-concurrent access, but we can disagree there...
Can I ask why not just do as I suggested from the opening post -- use FileShare and FileAccess to decide the flock() mode?
What is wrong with that? It seems to cover your "more important" scenario as well as the corruption of data on the most basic non-concurrent access.
Have I overlooked something? All the behavioral changes are "safe" -- you might get an error where you used to get a success (in undocumented ambiguously mapped behavior), but you no longer silently corrupt data.
rtestardi commentedon Mar 30, 2021
Let's call my suggestion 7. or 0., to differentiate it from your 1. - 6..
stephentoub commentedon Mar 30, 2021
That's what I just answered, and what you answered as well: "you might get an error where you used to get a success".
Ok.
danmoseley commentedon Mar 30, 2021
Leaving the locking aside, should we try to understand where the error is lost?
rtestardi commentedon Mar 30, 2021
Yes, both bugs should be fixed as called out in the opening post, since writes can fail for other reasons than the lock mode being wrong. It is possible the error is lost in code outside our control, (like the Linux buffer cache) -- I don't think we're clear on that yet especially once flock(UN) is added to the mix (which I did not do in my original experiments). It is also possible we need to add an fsync() to chase the error out of the buffer cache.
rtestardi commentedon Mar 30, 2021
IIRC, forcing write thru (O_SYNC or -o sync?) on the handle may make the error propagate correctly -- which would be more evidence that an fsync() might need to be added to .NET before close(). Someone would need to verify this. Of course, we want to be 100% sure we're not losing the error on close() as well.
filipnavara commentedon Jun 14, 2021
It would be nice to reach some consensus on the file sharing emulation. I started rebuilding my
File.CopyPR and this was one of the reasons why it was reverted last time.jeffhandley commentedon Jun 24, 2021
We haven't yet been able to get back to this issue to decide on the approach we'd want to take to work around this underlying behavior. It's unlikely that we will be able to get this resolved in time for 6.0 Preview 7, and I don't expect we'd consider it an RC1 candidate; so this is likely going to slip to 7.0.
filipnavara commentedon Jun 24, 2021
At this point I expected this to slip into the .NET 7 timeframe. I'll keep this on my radar and come back to ask later.