Skip to content

FileStream over write-only overlapped named pipe handle throws UnauthorizedAccessException on .NET 11 (regression from .NET 10) #131503

Closed

Description

@AndyGerlicher

Description

On .NET 11, constructing a FileStream over the write side of a write-only, overlapped named pipe throws UnauthorizedAccessException. The same code works on .NET 10 and earlier.

SafeFileHandle h = CreateNamedPipeW(name, PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, ...);
using var fs = new FileStream(h, FileAccess.Write, bufferSize: 4096, isAsync: true); // throws on .NET 11

The OSFileStreamStrategy constructor evaluates handle.CanSeek, which goes SafeFileHandle.Type → GetFileTypeCore() → GetPipeOrSocketType(). That probe requires read access on the handle, and a PIPE_ACCESS_OUTBOUND server handle is write-only, so it fails with ERROR_ACCESS_DENIED (5) and the constructor throws.

Note that nothing is ever read from this handle — the stream is opened FileAccess.Write only. The type/seek probe looks like unnecessary work for a write-only handle, and it is newly fatal.

Reproduction Steps

Minimal standalone repro (multi-targeted net10.0;net11.0, no external dependencies):

https://github.com/AndyGerlicher/net11-pipe-repro

git clone https://github.com/AndyGerlicher/net11-pipe-repro
cd net11-pipe-repro
dotnet run -f net10.0
dotnet run -f net11.0

Expected behavior

new FileStream(handle, FileAccess.Write, bufferSize, isAsync: true) succeeds, as it does on .NET 10:

[PASS] PIPE_ACCESS_OUTBOUND + FileStream   (what BuildXL does today)
[PASS] PIPE_ACCESS_DUPLEX   + FileStream   (workaround candidate 1)
[PASS] PIPE_ACCESS_OUTBOUND + NamedPipeServerStream (workaround candidate 2)

Actual behavior

On .NET 11 the first case fails:

Runtime: .NET 11.0.0-preview.6.26359.118

[FAIL] PIPE_ACCESS_OUTBOUND + FileStream   (what BuildXL does today)
       System.UnauthorizedAccessException: Access to the path is denied.
       at Microsoft.Win32.SafeHandles.SafeFileHandle.GetPipeOrSocketType()
       at Microsoft.Win32.SafeHandles.SafeFileHandle.GetFileTypeCore()
       at Microsoft.Win32.SafeHandles.SafeFileHandle.get_Type()
       at Microsoft.Win32.SafeHandles.SafeFileHandle.get_CanSeek()
       at System.IO.Strategies.OSFileStreamStrategy..ctor(SafeFileHandle handle, FileAccess access)
       at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(SafeFileHandle handle, FileAccess access, Boolean isAsync)
       at System.IO.FileStream..ctor(SafeFileHandle handle, FileAccess access, Int32 bufferSize, Boolean isAsync)
[PASS] PIPE_ACCESS_DUPLEX   + FileStream   (workaround candidate 1)
[PASS] PIPE_ACCESS_OUTBOUND + NamedPipeServerStream (workaround candidate 2)

Regression?

Yes — regression from .NET 10. Verified working on .NET 10.0.10, failing on .NET 11.0.0-preview.6.26359.118.

There does not appear to be an opt-out: setting System.IO.UseNet5CompatFileStream=true in runtimeconfig.json does not avoid the probe on .NET 11.

Known Workarounds

Both of these pass on .NET 10 and .NET 11 (both are exercised by the repro):

  1. Create the server handle with PIPE_ACCESS_DUPLEX instead of PIPE_ACCESS_OUTBOUND.
  2. Wrap the handle in NamedPipeServerStream instead of FileStream.

Configuration

  • .NET 11.0.0-preview.6.26359.118 (SDK 11.0.100-preview.6.26359.118) — fails
  • .NET 10.0.10 — works
  • Windows, x64
  • Not specific to a particular configuration as far as we can tell; the handle access mode is what matters.

Other information

This is not hypothetical — it breaks the BuildXL process sandbox, which CloudBuild/QuickBuild uses to execute every build target.

BuildXL.Processes.Internal.Pipes.CreateInheritablePipe creates the sandboxed child's stdin pipe with PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, and BuildXL.Processes.Internal.DetouredProcess.Start then wraps that handle:

if (writeHandle != null)
{
    FileStream stream = new FileStream(writeHandle, FileAccess.Write, m_bufferSize, isAsync: true);
    m_standardInputWriter = new StreamWriter(stream, m_standardInputEncoding, m_bufferSize) { AutoFlush = false };
}

That stdin pipe is created whenever the caller asks for stdout/stderr capture, which is the normal configuration.

Worth calling out how badly this presents in practice. SandboxedProcessFactory.ProcessStart does try { proc.Start(); } catch { proc?.Dispose(); throw; }, and DetouredProcess.Dispose() blocks indefinitely on this path — so the UnauthorizedAccessException never propagates and the already-created child process is never killed. The observable result in a distributed build is that every target launches its child process, emits zero bytes of stdout/stderr, records zero sandbox file-access events, and never completes; the build is eventually killed by a max-runtime watchdog with no error message anywhere. It took a full process dump to recover the exception above.

So while the deadlock is BuildXL's bug to fix, the trigger is this runtime behavior change, and the failure mode makes it very expensive to diagnose for anyone who hits it.

Activity

  1. dotnet-policy-service commented on Jul 28, 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. jhudsoncedaron commented on Jul 30, 2026

    @jhudsoncedaron

    I'm going to poke the bear. Try adding FILE_READ_ATTRIBUTES to your open operation. Bet it works.

    (Generally speaking it's a good idea to provide FILE_READ_ATTRIBUTES when opening a handle to something and passing it to high level code that isn't yours. I learned that the hard way ages ago.)

  3. added this to the 11.0.0 milestone on Aug 10, 2026
  4. adamsitnik commented on Aug 10, 2026

    @adamsitnik
    Member

    Hi @AndyGerlicher !

    Big thanks for a very detailed bug report. I was able to use your repro and confirm it.

    I've tried to find a proper way to fix it. Here are my findings:

    • CanSeek calls Type in order to figure out whether given file is seekable:

    internal bool CanSeek => !IsClosed && Type == FileHandleType.RegularFile;

    • ‎GetFileType returns FILE_TYPE_PIPE for both pipes and sockets, that is why we are calling GetNamedPipeInfo to figure out whether it's a pipe or socket:

    // When GetFileType returns FILE_TYPE_PIPE, the handle can be either a pipe or a socket.
    // Use GetNamedPipeInfo to determine if it's a pipe.
    uint flags;

    • We can't treat ERROR_ACCESS_DENIED as pipe, because it's not reliable and foremost this info is exposed by new public API (SafeFileHandle.Type), which should simply keep throwing the exception in case of missing permissions.

    int error = Marshal.GetLastPInvokeError();
    return error switch
    {
    Interop.Errors.ERROR_PIPE_NOT_CONNECTED => FileHandleType.Pipe,
    Interop.Errors.ERROR_INVALID_FUNCTION => FileHandleType.Socket,
    _ => throw Win32Marshal.GetExceptionForWin32Error(error, Path)
    };

    • We could use getsockopt to distinguish socket vs pipe, but it would add a dependency to additional library.

    • I think it will be best if we mimic what we do on Unix, namely try to seek by 0 from current position to figure out whether file is seekable:

    _canSeek = canSeek = Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0 ? NullableBool.True : NullableBool.False;

    This should unblock your scenario. I will just need to confirm that using different sys-call (SetFilePointer) won't regress the performance.

    Thanks,
    Adam

  5. self-assigned this
    on Aug 10, 2026
  6. added a commit that references this issue on Aug 11, 2026
    c82af78
  7. locked and limited conversation to collaborators on Sep 11, 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

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions