Repository navigation
Description
Description
On Linux, FileStream.Flush(flushToDisk: true) never reports a failed fsync. In .NET 10, SystemNative_FSync assigns the result of the comparison, not the result of the syscall:
while ((result =
#if defined(TARGET_OSX) && HAVE_F_FULLFSYNC
fcntl(fileDescriptor, F_FULLFSYNC)
#else
fsync(fileDescriptor)
#endif
< 0) && errno == EINTR);
return result;When fsync fails, result is 1. The managed caller checks < 0, so the error is dropped. The #else branch is the Linux path, so this is not specific to macOS.
#124725 fixed this in main. Its title, and the issue behind it (#124722), are about macOS and SMB, and it was not backported. release/10.0 and the v10.0.12 tag still have the code above.
This is a regression from .NET 5. In v5.0.0 the code was while ((result = fsync(ToFileDescriptor(fd))) < 0 && errno == EINTR);. Every release from v6.0.0 has the form above.
Why it matters
On NFS and other network file systems, the client caches writes. Errors from the server, such as ENOSPC, EDQUOT or EIO, are often reported only by fsync or close. close(2) recommends calling fsync first to catch them. On Unix, .NET also ignores close() errors (#111807). So Flush(flushToDisk: true) is the only API that could report these errors, and today it does not.
We hit this while building a storage service on .NET 10 that writes to customer NFS exports:
- A 64 MiB NFS export served by Linux knfsd, mounted with NFSv4.2, v4.1 and v3.
- We wrote 80 MiB through
FileStream, then calledFlush(flushToDisk: true). straceshowedfsync(31) = -1 ENOSPC.Flush(true)returned normally, and the file on the server was truncated.- A P/Invoke
fsyncon the same handle returned errno 28.
Reproduction Steps
This needs no NFS. strace injects the failure.
using System.IO;
Console.WriteLine($".NET {Environment.Version}");
using var fs = new FileStream("/tmp/repro.bin", FileMode.Create, FileAccess.Write);
fs.Write(new byte[4096]);
try { fs.Flush(flushToDisk: true); Console.WriteLine("RESULT: Flush(flushToDisk: true) returned without an exception"); }
catch (IOException e) { Console.WriteLine($"RESULT: IOException: {e.Message}"); }dotnet build repro.cs -o out
strace -f -qq -e trace=fsync -e inject=fsync:error=EIO ./out/reproResults on Linux x64, in Docker:
| Image | Runtime | strace | Result |
|---|---|---|---|
mcr.microsoft.com/dotnet/sdk:10.0 |
10.0.12 | fsync(31) = -1 EIO (INJECTED) |
no exception |
mcr.microsoft.com/dotnet/sdk:11.0-preview |
11.0.0 | fsync(25) = -1 EIO (INJECTED) |
IOException: Input/output error : '/tmp/repro.bin' |
Expected behavior
Flush(flushToDisk: true) throws IOException when fsync fails, as it does on .NET 5 and .NET 11.
Actual behavior
No exception. The data loss is silent.
Regression?
Yes. It works in .NET 5 and fails in every release from .NET 6 to .NET 10.12.
Known Workarounds
Call fsync through your own P/Invoke on SafeFileHandle, and check errno before disposing the stream.
Configuration
- .NET 10.0.12 and 10.0.9, from the
mcr.microsoft.com/dotnet/sdk:10.0image. - Linux x64: Docker Desktop with WSL2 kernel 6.6.87.
- The source of
release/10.0(as of 2026-10-02) and thev10.0.12tag still contain the old code.
Other information
Please consider backporting #124725 to release/10.0. For Linux, the fix is a single line:
while ((result = fsync(fileDescriptor)) < 0 && errno == EINTR);The macOS part of #124725 adds a fallback from F_FULLFSYNC to fsync. Servicing can take it or leave it out.
Related: #111807. That issue tracks close() errors being ignored on Unix, which is the same class of silent data loss on NFS.
FYI @stephentoub @adamsitnik, as the people involved in #124725 and #111807.
Activity
dotnet-policy-service commentedon Oct 5, 2026
Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.
Follow-up: real fsync for FileKeyFileStore (power-loss durability) (#10)
[release/10.0] Fix operator precedence bug in SystemNative_FSync caus…