Skip to content

Backport #124725 to release/10.0: FileStream.Flush(flushToDisk: true) ignores fsync errors on Linux (silent data loss on NFS) #135201

Description

@antonchaika

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 called Flush(flushToDisk: true).
  • strace showed fsync(31) = -1 ENOSPC.
  • Flush(true) returned normally, and the file on the server was truncated.
  • A P/Invoke fsync on 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/repro

Results 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.0 image.
  • Linux x64: Docker Desktop with WSL2 kernel 6.6.87.
  • The source of release/10.0 (as of 2026-10-02) and the v10.0.12 tag 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

  1. dotnet-policy-service commented on Oct 5, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-io
    See info in area-owners.md if you want to be subscribed.

  2. removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 6, 2026
  3. added this to the 10.0.x milestone on Oct 6, 2026
  4. self-assigned this
    on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions