diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs new file mode 100644 index 00000000000000..70bc434b2b3c25 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using Microsoft.Win32.SafeHandles; + +namespace System.Diagnostics.Tests +{ + public partial class ProcessHandlesTests + { + private static string GetSafeFileHandleId(SafeFileHandle handle) + { + if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status) != 0) + { + throw new Win32Exception(); + } + return FormattableString.Invariant($"{status.Dev}:{status.Ino}"); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 0734dcb66b6593..a95e36aa1a5c7c 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -181,5 +181,23 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) [LibraryImport(Interop.Libraries.Kernel32)] private static partial int ResumeThread(nint hThread); + + private static unsafe string GetSafeFileHandleId(SafeFileHandle handle) + { + const int MaxPath = 32_767; + char[] buffer = new char[MaxPath]; + uint result; + fixed (char* ptr = buffer) + { + result = Interop.Kernel32.GetFinalPathNameByHandle(handle, ptr, (uint)MaxPath, Interop.Kernel32.FILE_NAME_NORMALIZED); + } + + if (result == 0) + { + throw new Win32Exception(); + } + + return new string(buffer, 0, (int)result); + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index cb32430c3af591..be8b0268a6b1bf 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -540,5 +540,67 @@ public void InheritedHandles_ThrowsForDuplicates() Assert.Throws(() => Process.Start(startInfo)); } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void NonInheritedFileHandle_IsNotAvailableInChildProcess(bool inheritHandle) + { + string path = Path.GetTempFileName(); + try + { + // Open with FileShare.None so independent opens that honor .NET/OS sharing semantics are prevented. + using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None, FileOptions.None); + + // Verify the handle is valid in the parent process. + Assert.False(fileHandle.IsInvalid); + Assert.Equal(FileHandleType.RegularFile, fileHandle.Type); + nint rawHandle = fileHandle.DangerousGetHandle(); + + // When we start a process and limit handle inheritance, it's hard to reliably check that + // a given file handle/descriptor was not inherited, because the OS can simply re-use the + // exact same number for a different pipe/file/device. The idea here is to get something + // that can help us identify the given file handle and just compare the ID that the parent + // and child processes have obtained. + string id = GetSafeFileHandleId(fileHandle); + + RemoteInvokeOptions options = new() { CheckExitCode = true }; + // When inheritHandle is true, explicitly add the handle to the allow-list so it gets inherited. + // When inheritHandle is false, use an empty allow-list so no handles are inherited. + options.StartInfo.InheritedHandles = inheritHandle ? [fileHandle] : []; + + using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke( + static (string handleStr, string fileId, string inheritHandleStr) => + { + bool shouldBeInherited = bool.Parse(inheritHandleStr); + nint rawHandle = nint.Parse(handleStr); + using SafeFileHandle handle = new SafeFileHandle(rawHandle, ownsHandle: false); + + // Check whether it points to our file (the Operating System could reuse same value for a different file). + string? childId = null; + + try + { + childId = GetSafeFileHandleId(handle); + } + catch (Win32Exception) when (!shouldBeInherited) + { + // Handle is not a valid file handle in this process. + return RemoteExecutor.SuccessExitCode; + } + + Assert.Equal(shouldBeInherited, childId == fileId); + return RemoteExecutor.SuccessExitCode; + }, + rawHandle.ToString(), + id, + inheritHandle.ToString(), + options); + } + finally + { + File.Delete(path); + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index 9fe9c4c3f3dd72..ddc1d90710e085 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -68,11 +68,14 @@ Link="Common\Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs" /> + + +