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..b0a56cf3d8ac6b --- /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 partial string GetSafeFileHandleId(SafeFileHandle handle) + { + if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status) != 0) + { + throw new Win32Exception(); + } + return status.Ino.ToString(); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 0734dcb66b6593..2a6ed05d387531 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -181,5 +181,18 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) [LibraryImport(Interop.Libraries.Kernel32)] private static partial int ResumeThread(nint hThread); + + private static unsafe partial string GetSafeFileHandleId(SafeFileHandle handle) + { + const int MaxPath = 4096; + char[] buffer = new char[MaxPath]; + uint result; + fixed (char* ptr = buffer) + { + result = Interop.Kernel32.GetFinalPathNameByHandle(handle, ptr, (uint)MaxPath, Interop.Kernel32.FILE_NAME_NORMALIZED); + } + + return result > 0 ? new string(buffer, 0, (int)result) : handle.DangerousGetHandle().ToString(); + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs index cb32430c3af591..c339edef30224e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs @@ -540,5 +540,75 @@ 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 exclusive access so no other process can open the same file. + 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(); + + 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); + + if (handle.IsInvalid) + { + // Handle is invalid in the child: only acceptable when not inherited. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; + } + + // If the handle appears valid, check whether it points to our file. + // (the Operating System could reuse same value for a different file) + try + { + string childId = GetSafeFileHandleId(handle); + + if (childId == fileId) + { + // Handle points to our file — correct only when inherited. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode : RemoteExecutor.SuccessExitCode - 1; + } + } + catch + { + // Handle is not a valid file handle in this process. + return shouldBeInherited ? RemoteExecutor.SuccessExitCode - 1 : RemoteExecutor.SuccessExitCode; + } + + // Handle value was reused for a different file — not our handle, acceptable. + return RemoteExecutor.SuccessExitCode; + }, + rawHandle.ToString(), + id, + inheritHandle.ToString(), + options); + } + finally + { + File.Delete(path); + } + } + + private static partial string GetSafeFileHandleId(SafeFileHandle handle); } } 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" /> + + +