diff --git a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs index 8ffc9f91c9d336..3214e6a01593dd 100644 --- a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs +++ b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs @@ -44,6 +44,7 @@ internal static partial class StartupInfoOptions internal const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; internal const int CREATE_NO_WINDOW = 0x08000000; internal const int CREATE_NEW_PROCESS_GROUP = 0x00000200; + internal const int CREATE_SUSPENDED = 0x00000004; internal const int DETACHED_PROCESS = 0x00000008; } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs new file mode 100644 index 00000000000000..31300bc6adc5e7 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + internal sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeJobHandle() : base(true) { } + + protected override bool ReleaseHandle() => Interop.Kernel32.CloseHandle(handle); + } + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + internal static partial SafeJobHandle CreateJobObjectW(IntPtr lpJobAttributes, IntPtr lpName); + + internal const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + + internal enum JOBOBJECTINFOCLASS + { + JobObjectExtendedLimitInformation = 9 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IO_COUNTERS + { + internal ulong ReadOperationCount; + internal ulong WriteOperationCount; + internal ulong OtherOperationCount; + internal ulong ReadTransferCount; + internal ulong WriteTransferCount; + internal ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + internal long PerProcessUserTimeLimit; + internal long PerJobUserTimeLimit; + internal uint LimitFlags; + internal nuint MinimumWorkingSetSize; + internal nuint MaximumWorkingSetSize; + internal uint ActiveProcessLimit; + internal nuint Affinity; + internal uint PriorityClass; + internal uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + internal JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + internal IO_COUNTERS IoInfo; + internal nuint ProcessMemoryLimit; + internal nuint JobMemoryLimit; + internal nuint PeakProcessMemoryUsed; + internal nuint PeakJobMemoryUsed; + } + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetInformationJobObject(SafeJobHandle hJob, JOBOBJECTINFOCLASS JobObjectInfoClass, ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, uint cbJobObjectInfoLength); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool AssignProcessToJobObject(SafeJobHandle hJob, SafeProcessHandle hProcess); + + internal const int PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D; + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread.cs new file mode 100644 index 00000000000000..9da3a07bf2f451 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + internal static partial uint ResumeThread(IntPtr hThread); + } +} diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 549f363369445b..7257744b6de56c 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -255,6 +255,8 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable< public string FileName { get { throw null; } set { } } public System.Collections.Generic.IList? InheritedHandles { get { throw null; } set { } } [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] + public bool KillOnParentExit { get { throw null; } set { } } + [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] public bool LoadUserProfile { get { throw null; } set { } } [System.CLSCompliantAttribute(false)] [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index d658f38f73f2ea..29e915a0803c0b 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -14,11 +14,38 @@ namespace Microsoft.Win32.SafeHandles { public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid { + // Static job object used for KillOnParentExit functionality. + // All child processes with KillOnParentExit=true are assigned to this job. + // The job handle is intentionally never closed - it should live for the + // lifetime of the process. When this process exits, the job object is destroyed + // by the OS, which terminates all child processes in the job. + private static readonly Lazy s_killOnParentExitJob = new(CreateKillOnParentExitJob); + protected override bool ReleaseHandle() { return Interop.Kernel32.CloseHandle(handle); } + private static unsafe Interop.Kernel32.SafeJobHandle CreateKillOnParentExitJob() + { + Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.BasicLimitInformation.LimitFlags = Interop.Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + Interop.Kernel32.SafeJobHandle jobHandle = Interop.Kernel32.CreateJobObjectW(IntPtr.Zero, IntPtr.Zero); + if (jobHandle.IsInvalid || !Interop.Kernel32.SetInformationJobObject( + jobHandle, + Interop.Kernel32.JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation, + ref limitInfo, + (uint)sizeof(Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION))) + { + int error = Marshal.GetLastWin32Error(); + jobHandle.Dispose(); + throw new Win32Exception(error); + } + + return jobHandle; + } + private static Func? s_startWithShellExecute; internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, @@ -47,12 +74,15 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S // Inheritable copies of the child handles for CreateProcess bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; - bool hasInheritedHandles = inheritedHandles is not null; + bool restrictInheritedHandles = inheritedHandles is not null; + bool killOnParentExit = startInfo.KillOnParentExit; + bool logon = !string.IsNullOrEmpty(startInfo.UserName); - // When InheritedHandles is set, we use PROC_THREAD_ATTRIBUTE_HANDLE_LIST to restrict inheritance. + // When InheritedHandles is set, we use PROC_THREAD_ATTRIBUTE_HANDLE_LIST to restrict inheritance + // or pass bInheritHandles=false when there are no valid handles to inherit. // For that, we need a reader lock (concurrent starts with different explicit lists are safe). // When InheritedHandles is not set, we use the existing approach with a writer lock. - if (hasInheritedHandles) + if (restrictInheritedHandles) { ProcessUtils.s_processStartLock.EnterReadLock(); } @@ -68,10 +98,11 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S void* attributeListBuffer = null; SafeHandle?[]? handlesToRelease = null; IntPtr* handlesToInherit = null; + IntPtr* jobHandles = null; try { - startupInfoEx.StartupInfo.cb = hasInheritedHandles ? sizeof(Interop.Kernel32.STARTUPINFOEX) : sizeof(Interop.Kernel32.STARTUPINFO); + startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFO); ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref startupInfoEx.StartupInfo.hStdInput, ref stdinRefAdded); ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref startupInfoEx.StartupInfo.hStdOutput, ref stdoutRefAdded); @@ -87,7 +118,7 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S } // set up the creation flags parameter - int creationFlags = hasInheritedHandles ? Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT : 0; + int creationFlags = 0; if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW; if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP; if (startInfo.StartDetached) creationFlags |= Interop.Advapi32.StartupInfoOptions.DETACHED_PROCESS; @@ -106,30 +137,27 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S workingDirectory = null; } - // When InheritedHandles is set, build a PROC_THREAD_ATTRIBUTE_HANDLE_LIST to restrict - // inheritance to only the explicitly specified handles. - int handleCount = 0; - if (hasInheritedHandles) - { - int maxHandleCount = 3 + inheritedHandles!.Length; - handlesToInherit = (IntPtr*)NativeMemory.Alloc((nuint)maxHandleCount, (nuint)sizeof(IntPtr)); - Span handlesToInheritSpan = new Span(handlesToInherit, maxHandleCount); + // By default, all handles are inherited. + bool bInheritHandles = true; - // Add valid effective stdio handles (already made inheritable via DuplicateAsInheritableIfNeeded) - AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdInput, handlesToInheritSpan, ref handleCount); - AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdOutput, handlesToInheritSpan, ref handleCount); - AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdError, handlesToInheritSpan, ref handleCount); - - EnableInheritanceAndAddRef(inheritedHandles, handlesToInheritSpan, ref handleCount, ref handlesToRelease); - BuildProcThreadAttributeList(handlesToInherit, handleCount, ref attributeListBuffer); + // Extended Startup Info can be configured only for the non-logon path + if (!logon) + { + if (ConfigureExtendedStartupInfo(inheritedHandles, killOnParentExit, + in startupInfoEx, ref attributeListBuffer, + ref handlesToInherit, ref handlesToRelease, ref bInheritHandles, + ref jobHandles)) + { + creationFlags |= Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT; + startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); + startupInfoEx.lpAttributeList = attributeListBuffer; + } } - startupInfoEx.lpAttributeList = attributeListBuffer; - bool retVal; int errorCode = 0; - if (startInfo.UserName.Length != 0) + if (logon) { if (startInfo.Password != null && startInfo.PasswordInClearText != null) { @@ -153,8 +181,17 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S // CreateProcessWithLogonW does not support STARTUPINFOEX. CreateProcessWithTokenW docs mention STARTUPINFOEX, // but they don't mention that EXTENDED_STARTUPINFO_PRESENT is not supported anyway. // CreateProcessAsUserW supports both, but it's too restrictive and simply different than CreateProcessWithLogonW in many ways. - Debug.Assert(!hasInheritedHandles, "Inheriting handles is not supported when starting with alternate credentials."); + Debug.Assert(!restrictInheritedHandles, "Inheriting handles is not supported when starting with alternate credentials."); Debug.Assert(startupInfoEx.StartupInfo.cb == sizeof(Interop.Kernel32.STARTUPINFO)); + Debug.Assert((creationFlags & Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT) == 0); + + // When KillOnParentExit is set and we use CreateProcessWithLogonW (which doesn't support + // PROC_THREAD_ATTRIBUTE_JOB_LIST), we create the process suspended, assign it to the job, + // then resume it. + if (killOnParentExit) + { + creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_SUSPENDED; + } commandLine.NullTerminate(); fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) @@ -197,9 +234,6 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S fixed (char* environmentBlockPtr = environmentBlock) fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) { - // When InheritedHandles is set but handleCount is 0 (e.g. empty list, no stdio), - // pass false to prevent all inheritable handles from leaking to the child. - bool bInheritHandles = !hasInheritedHandles || handleCount > 0; retVal = Interop.Kernel32.CreateProcess( null, // we don't need this since all the info is in commandLine commandLinePtr, // pointer to the command line string @@ -218,9 +252,16 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S } if (!IsInvalidHandle(processInfo.hProcess)) + { Marshal.InitHandle(procSH, processInfo.hProcess); - if (!IsInvalidHandle(processInfo.hThread)) - Interop.Kernel32.CloseHandle(processInfo.hThread); + + // When the process was started suspended for KillOnParentExit with CreateProcessWithLogonW, + // assign it to the job object and then resume the thread. + if (killOnParentExit && logon) + { + AssignJobAndResumeThread(processInfo.hThread, procSH); + } + } if (!retVal) { @@ -238,6 +279,9 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S } finally { + if (!IsInvalidHandle(processInfo.hThread)) + Interop.Kernel32.CloseHandle(processInfo.hThread); + // If the provided handle was inheritable, just release the reference we added. // Otherwise if we created a valid duplicate, close it. @@ -257,6 +301,7 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S Interop.Kernel32.CloseHandle(startupInfoEx.StartupInfo.hStdError); NativeMemory.Free(handlesToInherit); + NativeMemory.Free(jobHandles); if (attributeListBuffer is not null) { @@ -269,7 +314,7 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S DisableInheritanceAndRelease(handlesToRelease); } - if (hasInheritedHandles) + if (restrictInheritedHandles) { ProcessUtils.s_processStartLock.ExitReadLock(); } @@ -421,18 +466,55 @@ private static void AddToInheritListIfValid(nint handle, Span handlesToInh handlesToInherit[handleCount++] = handle; } - /// - /// Creates and populates a PROC_THREAD_ATTRIBUTE_LIST with a PROC_THREAD_ATTRIBUTE_HANDLE_LIST entry. - /// - private static unsafe void BuildProcThreadAttributeList( - IntPtr* handlesToInherit, - int handleCount, - ref void* attributeListBuffer) + private static unsafe bool ConfigureExtendedStartupInfo(SafeHandle[]? inheritedHandles, bool killOnParentExit, + in Interop.Kernel32.STARTUPINFOEX startupInfoEx, ref void* attributeListBuffer, + ref nint* handlesToInherit, ref SafeHandle?[]? handlesToRelease, ref bool bInheritHandles, + ref nint* jobHandles) { + // Determine the number of attributes we need to set in the proc thread attribute list. + int attributeCount = 0; + + int handleCount = 0; + if (inheritedHandles is not null) + { + int maxHandleCount = 3 + inheritedHandles.Length; + handlesToInherit = (IntPtr*)NativeMemory.Alloc((nuint)maxHandleCount, (nuint)sizeof(IntPtr)); + Span handlesToInheritSpan = new Span(handlesToInherit, maxHandleCount); + + // Add valid effective stdio handles (already made inheritable via DuplicateAsInheritableIfNeeded) + AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdInput, handlesToInheritSpan, ref handleCount); + AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdOutput, handlesToInheritSpan, ref handleCount); + AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdError, handlesToInheritSpan, ref handleCount); + + EnableInheritanceAndAddRef(inheritedHandles, handlesToInheritSpan, ref handleCount, ref handlesToRelease); + + if (handleCount == 0) + { + // When InheritedHandles is set but handleCount is 0 (e.g. all standard handles are invalid), + // pass false to prevent all inheritable handles from leaking to the child. + bInheritHandles = false; + } + else + { + attributeCount++; // PROC_THREAD_ATTRIBUTE_HANDLE_LIST + } + } + + if (killOnParentExit) + { + jobHandles = (IntPtr*)NativeMemory.Alloc(1, (nuint)sizeof(IntPtr)); + jobHandles[0] = s_killOnParentExitJob.Value.DangerousGetHandle(); + + attributeCount++; // PROC_THREAD_ATTRIBUTE_JOB_LIST + } + + if (attributeCount == 0) + { + return false; + } + nuint size = 0; - int attributeCount = handleCount > 0 ? 1 : 0; Interop.Kernel32.InitializeProcThreadAttributeList(null, attributeCount, 0, ref size); - attributeListBuffer = NativeMemory.Alloc(size); if (!Interop.Kernel32.InitializeProcThreadAttributeList(attributeListBuffer, attributeCount, 0, ref size)) @@ -451,6 +533,20 @@ private static unsafe void BuildProcThreadAttributeList( { throw new Win32Exception(Marshal.GetLastWin32Error()); } + + if (killOnParentExit && !Interop.Kernel32.UpdateProcThreadAttribute( + attributeListBuffer, + 0, + (IntPtr)Interop.Kernel32.PROC_THREAD_ATTRIBUTE_JOB_LIST, + jobHandles, + (nuint)sizeof(IntPtr), + null, + null)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + return true; } private static void EnableInheritanceAndAddRef( @@ -502,6 +598,30 @@ private static void DisableInheritanceAndRelease(SafeHandle?[] handlesToRelease) } } + private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle procSH) + { + Debug.Assert(!IsInvalidHandle(hThread), "Thread handle must be valid for suspended process."); + + try + { + if (!Interop.Kernel32.AssignProcessToJobObject(s_killOnParentExitJob.Value, procSH)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (Interop.Kernel32.ResumeThread(hThread) == 0xFFFFFFFF) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + catch + { + // If we fail to assign to the job or resume the thread, terminate the process. + Interop.Kernel32.TerminateProcess(procSH, -1); + throw; + } + } + private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this); private bool SignalCore(PosixSignal signal) diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 6929fc7f326567..f4bba47d523bb4 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -219,6 +219,9 @@ The InheritedHandles property cannot be used with UseShellExecute or UserName. + + The KillOnParentExit property cannot be used with UseShellExecute. + The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties. diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index b32cd9a542d787..2c1c9e0be2eb23 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -105,6 +105,10 @@ Link="Common\Interop\Windows\Kernel32\Interop.GetThreadTimes.cs" /> + + public IList? InheritedHandles { get; set; } + /// + /// Gets or sets a value indicating whether the child process should be terminated when the parent process exits. + /// + /// + /// + /// When this property is set to , the operating system will automatically terminate + /// the child process when the parent process exits, regardless of whether the parent exits gracefully or crashes. + /// + /// + /// This property cannot be used together with set to . + /// + /// + /// On Windows, this is implemented using Job Objects with the + /// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag. + /// + /// + /// to terminate the child process when the parent exits; otherwise, . The default is . + [SupportedOSPlatform("windows")] + public bool KillOnParentExit { get; set; } + public Encoding? StandardInputEncoding { get; set; } public Encoding? StandardErrorEncoding { get; set; } @@ -427,6 +448,13 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite throw new InvalidOperationException(SR.InheritedHandlesRequiresCreateProcess); } +#pragma warning disable CA1416 // KillOnParentExit getter works on all platforms; the attribute guards the actual effect + if (KillOnParentExit && UseShellExecute) +#pragma warning restore CA1416 + { + throw new InvalidOperationException(SR.KillOnParentExitCannotBeUsedWithUseShellExecute); + } + if (InheritedHandles is not null) { IList list = InheritedHandles; diff --git a/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs b/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs new file mode 100644 index 00000000000000..34a100e7e80bbf --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.Diagnostics.Tests +{ + [PlatformSpecific(TestPlatforms.Windows)] + public class KillOnParentExitTests : ProcessTestBase + { + [Fact] + public void KillOnParentExit_DefaultsToFalse() + { + ProcessStartInfo startInfo = new(); + + Assert.False(startInfo.KillOnParentExit); + } + + [Fact] + public void KillOnParentExit_CanBeSetToTrue() + { + ProcessStartInfo startInfo = new() + { + KillOnParentExit = true + }; + + Assert.True(startInfo.KillOnParentExit); + } + + [Fact] + public void KillOnParentExit_WithUseShellExecute_Throws() + { + ProcessStartInfo startInfo = new("dummy") + { + KillOnParentExit = true, + UseShellExecute = true + }; + + Assert.Throws(() => Process.Start(startInfo)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void KillOnParentExit_ProcessStartsAndExitsNormally() + { + using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + process.StartInfo.KillOnParentExit = true; + process.Start(); + + Assert.True(process.WaitForExit(WaitInMS)); + Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, false)] // (false, true) is tested by ProcessHandleTests + public void KillOnParentExit_KillsTheChild_WhenParentExits(bool enabled, bool restrictInheritance) + { + RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false }; + remoteInvokeOptions.StartInfo.RedirectStandardOutput = true; + remoteInvokeOptions.StartInfo.RedirectStandardInput = true; + + using RemoteInvokeHandle childHandle = RemoteExecutor.Invoke( + (enabledStr, limitInheritanceStr) => + { + using Process grandChild = CreateProcessLong(); + grandChild.StartInfo.KillOnParentExit = bool.Parse(enabledStr); + grandChild.StartInfo.InheritedHandles = bool.Parse(limitInheritanceStr) ? [] : null; + + grandChild.Start(); + Console.WriteLine(grandChild.Id); + + // This will block the child until parent provides input. + _ = Console.ReadLine(); + }, + enabled.ToString(), + restrictInheritance.ToString(), + remoteInvokeOptions); + + int grandChildPid = int.Parse(childHandle.Process.StandardOutput.ReadLine()); + + // Obtain a Process instance before the child exits to avoid PID reuse issues. + using Process grandchild = Process.GetProcessById(grandChildPid); + + try + { + childHandle.Process.StandardInput.WriteLine("You can exit now."); + + Assert.True(childHandle.Process.WaitForExit(WaitInMS)); + // Use shorter wait time when the process is expected to survive + Assert.Equal(enabled, grandchild.WaitForExit(enabled ? WaitInMS : 300)); + } + finally + { + grandchild.Kill(); + } + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, false)] // (false, true) is tested by ProcessHandleTests + public void KillOnParentExit_KillsTheChild_WhenParentIsKilled(bool enabled, bool restrictInheritance) + { + RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false }; + remoteInvokeOptions.StartInfo.RedirectStandardOutput = true; + remoteInvokeOptions.StartInfo.RedirectStandardInput = true; + + using RemoteInvokeHandle childHandle = RemoteExecutor.Invoke( + (enabledStr, limitInheritanceStr) => + { + using Process grandChild = CreateProcessLong(); + grandChild.StartInfo.KillOnParentExit = bool.Parse(enabledStr); + grandChild.StartInfo.InheritedHandles = bool.Parse(limitInheritanceStr) ? [] : null; + + grandChild.Start(); + Console.WriteLine(grandChild.Id); + + // This will block the child until parent kills it. + _ = Console.ReadLine(); + }, + enabled.ToString(), + restrictInheritance.ToString(), + remoteInvokeOptions); + + int grandChildPid = int.Parse(childHandle.Process.StandardOutput.ReadLine()); + + // Obtain a Process instance before the child is killed to avoid PID reuse issues. + using Process grandchild = Process.GetProcessById(grandChildPid); + + try + { + childHandle.Process.Kill(); + + Assert.True(childHandle.Process.WaitForExit(WaitInMS)); + Assert.NotEqual(0, childHandle.Process.ExitCode); + // Use shorter wait time when the process is expected to survive + Assert.Equal(enabled, grandchild.WaitForExit(enabled ? WaitInMS : 300)); + } + finally + { + grandchild.Kill(); + } + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, false)] // (false, true) is tested by ProcessHandleTests + [OuterLoop("May create a memory dump")] + public void KillOnParentExit_KillsTheChild_WhenParentCrashes(bool enabled, bool restrictInheritance) + { + RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false }; + remoteInvokeOptions.StartInfo.RedirectStandardOutput = true; + remoteInvokeOptions.StartInfo.RedirectStandardInput = true; + + using RemoteInvokeHandle childHandle = RemoteExecutor.Invoke( + (enabledStr, limitInheritanceStr) => + { + using Process grandChild = CreateProcessLong(); + grandChild.StartInfo.KillOnParentExit = bool.Parse(enabledStr); + grandChild.StartInfo.InheritedHandles = bool.Parse(limitInheritanceStr) ? [] : null; + + grandChild.Start(); + Console.WriteLine(grandChild.Id); + + // This will block the child until parent writes input. + _ = Console.ReadLine(); + + // Guaranteed Access Violation - write to null pointer + Marshal.WriteInt32(IntPtr.Zero, 42); + }, + enabled.ToString(), + restrictInheritance.ToString(), + remoteInvokeOptions); + + int grandChildPid = int.Parse(childHandle.Process.StandardOutput.ReadLine()); + + // Obtain a Process instance before the child crashes to avoid PID reuse issues. + using Process grandchild = Process.GetProcessById(grandChildPid); + + try + { + childHandle.Process.StandardInput.WriteLine("One AccessViolationException please."); + + Assert.True(childHandle.Process.WaitForExit(WaitInMS)); + Assert.NotEqual(0, childHandle.Process.ExitCode); + // Use shorter wait time when the process is expected to survive + Assert.Equal(enabled, grandchild.WaitForExit(enabled ? WaitInMS : 300)); + } + finally + { + grandchild.Kill(); + } + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 0e7fdba68ceb0a..92d2c7bfde2fbe 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.IO; using System.IO.Pipes; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; @@ -32,11 +31,13 @@ public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles() } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData(false)] - [InlineData(true)] - public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInvalidHandles(bool restrictHandles) + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInvalidHandles(bool restrictHandles, bool killOnParentExit) { - using Process process = CreateProcess(arg => + using Process process = CreateProcess((inheritanceArg, killArg) => { using (Process childProcess = CreateProcess(() => { @@ -47,7 +48,8 @@ public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInva return RemoteExecutor.SuccessExitCode; })) { - childProcess.StartInfo.InheritedHandles = bool.Parse(arg) ? [] : null; + childProcess.StartInfo.InheritedHandles = bool.Parse(inheritanceArg) ? [] : null; + childProcess.StartInfo.KillOnParentExit = bool.Parse(killArg); childProcess.Start(); try @@ -60,7 +62,7 @@ public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInva childProcess.Kill(); } } - }, restrictHandles.ToString()); + }, restrictHandles.ToString(), killOnParentExit.ToString()); Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo)); } @@ -154,7 +156,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) // As soon as SafeProcessHandle.WaitForExit* are implemented (#126293), we can use them instead. using Process process = Process.GetProcessById(processInfo.dwProcessId); - if (ResumeThread(processInfo.hThread) == -1) + if (Interop.Kernel32.ResumeThread(processInfo.hThread) == 0xFFFFFFFF) { throw new Win32Exception(); } @@ -180,9 +182,6 @@ 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; diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs index 11b67168507562..ecd5a5b3b0bf7e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs @@ -500,14 +500,17 @@ public void TestWorkingDirectoryPropertyInChildProcess() }, workingDirectory, new RemoteInvokeOptions { StartInfo = psi }).Dispose(); } - [ConditionalFact(typeof(ProcessStartInfoTests), nameof(IsAdmin_IsNotNano_RemoteExecutorIsSupported))] // Nano has no "netapi32.dll", Admin rights are required + [ConditionalTheory(typeof(ProcessStartInfoTests), nameof(IsAdmin_IsNotNano_RemoteExecutorIsSupported))] // Nano has no "netapi32.dll", Admin rights are required [PlatformSpecific(TestPlatforms.Windows)] [OuterLoop("Requires admin privileges")] [ActiveIssue("https://github.com/dotnet/runtime/issues/80019", TestRuntimes.Mono)] - public void TestUserCredentialsPropertiesOnWindows() + [InlineData(true)] + [InlineData(false)] + public void TestUserCredentialsPropertiesOnWindows(bool killOnParentExit) { using Process longRunning = CreateProcessLong(); longRunning.StartInfo.LoadUserProfile = true; + longRunning.StartInfo.KillOnParentExit = killOnParentExit; using TestProcessState testAccountCleanup = CreateUserAndExecute(longRunning, Setup, Cleanup); 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 b9df15c7d9a47b..5c345d299a1ac8 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 @@ -39,6 +39,7 @@ + @@ -70,6 +71,8 @@ Link="Common\Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs" /> +