Skip to content

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

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.TypeGetFileTypeCore()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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions