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):
- Create the server handle with
PIPE_ACCESS_DUPLEX instead of PIPE_ACCESS_OUTBOUND.
- 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.
Description
On .NET 11, constructing a
FileStreamover the write side of a write-only, overlapped named pipe throwsUnauthorizedAccessException. The same code works on .NET 10 and earlier.The
OSFileStreamStrategyconstructor evaluateshandle.CanSeek, which goesSafeFileHandle.Type→GetFileTypeCore()→GetPipeOrSocketType(). That probe requires read access on the handle, and aPIPE_ACCESS_OUTBOUNDserver handle is write-only, so it fails withERROR_ACCESS_DENIED(5) and the constructor throws.Note that nothing is ever read from this handle — the stream is opened
FileAccess.Writeonly. 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
Expected behavior
new FileStream(handle, FileAccess.Write, bufferSize, isAsync: true)succeeds, as it does on .NET 10:Actual behavior
On .NET 11 the first case fails:
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=trueinruntimeconfig.jsondoes 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):
PIPE_ACCESS_DUPLEXinstead ofPIPE_ACCESS_OUTBOUND.NamedPipeServerStreaminstead ofFileStream.Configuration
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.CreateInheritablePipecreates the sandboxed child's stdin pipe withPIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, andBuildXL.Processes.Internal.DetouredProcess.Startthen wraps that handle: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.ProcessStartdoestry { proc.Start(); } catch { proc?.Dispose(); throw; }, andDetouredProcess.Dispose()blocks indefinitely on this path — so theUnauthorizedAccessExceptionnever 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.