From a08204b1c39fe8198ec2209925d3686855f7a77e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:16:21 +0000 Subject: [PATCH 1/9] Initial plan From 51103b18c648068e801ebc69bd8d6f29f27f8ae0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:22:07 +0000 Subject: [PATCH 2/9] Fix Process.KillTree to avoid throwing exceptions for access denied Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/Diagnostics/ProcessManager.Windows.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs index c27da2b283ed42..6306df8f276f21 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs @@ -253,7 +253,6 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro } // If the handle is invalid because the process has exited, only throw an exception if throwIfExited is true. - // Assume the process is still running if the error was ERROR_ACCESS_DENIED for better performance if (result != Interop.Errors.ERROR_ACCESS_DENIED && !IsProcessRunning(processId)) { if (throwIfExited) @@ -265,6 +264,14 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro return SafeProcessHandle.InvalidHandle; } } + + // When throwIfExited is false, return an invalid handle for access denied errors to avoid throwing exceptions + // during process enumeration where access denied is common and expected (e.g., for system processes). + if (!throwIfExited && result == Interop.Errors.ERROR_ACCESS_DENIED) + { + return SafeProcessHandle.InvalidHandle; + } + throw new Win32Exception(result); } From 5b1fbcb041766733fe1e6d2aca68be3a13bbff3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:49:36 +0000 Subject: [PATCH 3/9] Add test for Kill(true) minimal exceptions behavior Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../tests/ProcessTests.Windows.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs index 7850ca8afcbd7d..11992f29453dbe 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs @@ -5,6 +5,8 @@ using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; using Xunit; @@ -55,5 +57,98 @@ private static unsafe void ReEnableCtrlCHandlerIfNeeded(PosixSignal signal) } } } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void Kill_EntireProcessTree_MinimalExceptions() + { + // This test validates that Kill(true) doesn't throw excessive exceptions internally + // during process enumeration, which causes severe performance degradation with debugger attached. + // See https://github.com/dotnet/runtime/issues/121279 + + int exceptionCount = 0; + EventHandler handler = + (sender, e) => Interlocked.Increment(ref exceptionCount); + + AppDomain.CurrentDomain.FirstChanceException += handler; + + try + { + // Create a process tree based on the repro from the issue: + // Main process spawns child1, child1 spawns child2 + RemoteInvokeHandle handle = RemoteExecutor.Invoke(CreateProcessTreeAndWait); + Process parentProcess = handle.Process; + + try + { + // Wait a bit to ensure the process tree is established + Thread.Sleep(1500); + + // Reset the counter before killing + exceptionCount = 0; + + // Kill the entire process tree + parentProcess.Kill(entireProcessTree: true); + + // Wait for process to exit + Assert.True(parentProcess.WaitForExit(Helpers.PassingTestTimeoutMilliseconds)); + + // The fix should ensure that we don't throw exceptions for each system process + // that we can't access during enumeration. Allow up to 5 exceptions for edge cases + // (e.g., processes that exit during enumeration, rare error conditions) + Assert.True(exceptionCount <= 5, + $"Expected no more than 5 exceptions during Kill(true), but got {exceptionCount}"); + } + finally + { + try + { + if (!parentProcess.HasExited) + { + parentProcess.Kill(); + } + handle.Dispose(); + } + catch + { + // Best effort cleanup + } + } + } + finally + { + AppDomain.CurrentDomain.FirstChanceException -= handler; + } + } + + private static int CreateProcessTreeAndWait() + { + // This mimics the repro code from the issue + // Create child1 which will create child2 + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = RemoteExecutor.HostRunner, + Arguments = $"exec \"{RemoteExecutor.Path}\" {typeof(ProcessTests).Assembly.Location} {nameof(SleepForever)}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + using (Process child1 = Process.Start(psi)) + { + // Wait a bit to ensure child1 is running + Thread.Sleep(500); + + // Keep this process alive + Thread.Sleep(Timeout.Infinite); + } + + return RemoteExecutor.SuccessExitCode; + } + + private static int SleepForever() + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + } } } From 0641f8c3d8a208529d01f4d67a3e4b5f90e109f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:03:12 +0000 Subject: [PATCH 4/9] Refactor OpenProcess error handling and rename parameter to throwOnError Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Diagnostics/ProcessManager.Windows.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs index 6306df8f276f21..4a93112868d0c0 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs @@ -236,7 +236,7 @@ static unsafe ProcessManager() } } - public static SafeProcessHandle OpenProcess(int processId, int access, bool throwIfExited) + public static SafeProcessHandle OpenProcess(int processId, int access, bool throwOnError) { SafeProcessHandle processHandle = Interop.Kernel32.OpenProcess(access, false, processId); int result = Marshal.GetLastWin32Error(); @@ -252,10 +252,19 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro throw new Win32Exception(Interop.Errors.ERROR_ACCESS_DENIED); } - // If the handle is invalid because the process has exited, only throw an exception if throwIfExited is true. - if (result != Interop.Errors.ERROR_ACCESS_DENIED && !IsProcessRunning(processId)) + // Handle different error conditions based on throwOnError parameter. + // When throwOnError is false (e.g., during process enumeration), return invalid handle + // for common/expected errors instead of throwing to avoid excessive exception overhead. + if (result is Interop.Errors.ERROR_ACCESS_DENIED) { - if (throwIfExited) + if (!throwOnError) + { + return SafeProcessHandle.InvalidHandle; + } + } + else if (!IsProcessRunning(processId)) + { + if (throwOnError) { throw new InvalidOperationException(SR.Format(SR.ProcessHasExited, processId.ToString())); } @@ -265,13 +274,6 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro } } - // When throwIfExited is false, return an invalid handle for access denied errors to avoid throwing exceptions - // during process enumeration where access denied is common and expected (e.g., for system processes). - if (!throwIfExited && result == Interop.Errors.ERROR_ACCESS_DENIED) - { - return SafeProcessHandle.InvalidHandle; - } - throw new Win32Exception(result); } From 2507431b120262e9b7d42a00e1523fdc61493c6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:12:52 +0000 Subject: [PATCH 5/9] Rename parameter from throwOnError to throwIfInaccessible for clarity Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Diagnostics/ProcessManager.Windows.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs index 4a93112868d0c0..c65e849efab5e9 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs @@ -236,7 +236,7 @@ static unsafe ProcessManager() } } - public static SafeProcessHandle OpenProcess(int processId, int access, bool throwOnError) + public static SafeProcessHandle OpenProcess(int processId, int access, bool throwIfInaccessible) { SafeProcessHandle processHandle = Interop.Kernel32.OpenProcess(access, false, processId); int result = Marshal.GetLastWin32Error(); @@ -252,19 +252,19 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro throw new Win32Exception(Interop.Errors.ERROR_ACCESS_DENIED); } - // Handle different error conditions based on throwOnError parameter. - // When throwOnError is false (e.g., during process enumeration), return invalid handle + // Handle different error conditions based on throwIfInaccessible parameter. + // When throwIfInaccessible is false (e.g., during process enumeration), return invalid handle // for common/expected errors instead of throwing to avoid excessive exception overhead. if (result is Interop.Errors.ERROR_ACCESS_DENIED) { - if (!throwOnError) + if (!throwIfInaccessible) { return SafeProcessHandle.InvalidHandle; } } else if (!IsProcessRunning(processId)) { - if (throwOnError) + if (throwIfInaccessible) { throw new InvalidOperationException(SR.Format(SR.ProcessHasExited, processId.ToString())); } From a26e46bdb74e05352a7d4632f50faafca45dc40f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:05:53 +0000 Subject: [PATCH 6/9] Handle additional permission-related errors to fix test for non-admin users Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/Diagnostics/ProcessManager.Windows.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs index c65e849efab5e9..8e96c30e065b60 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs @@ -255,7 +255,7 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro // Handle different error conditions based on throwIfInaccessible parameter. // When throwIfInaccessible is false (e.g., during process enumeration), return invalid handle // for common/expected errors instead of throwing to avoid excessive exception overhead. - if (result is Interop.Errors.ERROR_ACCESS_DENIED) + if (IsProcessAccessError(result)) { if (!throwIfInaccessible) { @@ -277,6 +277,16 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro throw new Win32Exception(result); } + private static bool IsProcessAccessError(int errorCode) + { + // Common errors when trying to access processes we don't have permission to query + return errorCode == Interop.Errors.ERROR_ACCESS_DENIED || + errorCode == Interop.Errors.ERROR_INVALID_ACCESS || + errorCode == Interop.Errors.ERROR_NETWORK_ACCESS_DENIED || + errorCode == Interop.Errors.ERROR_NO_SUCH_PRIVILEGE || + errorCode == Interop.Errors.ERROR_PRIVILEGE_NOT_HELD; + } + public static SafeThreadHandle OpenThread(int threadId, int access) { SafeThreadHandle threadHandle = Interop.Kernel32.OpenThread(access, false, threadId); From 1aac39f452f4d3d5745caa6cce79c9f4efd54a33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:28:55 +0000 Subject: [PATCH 7/9] Simplify OpenProcess: return invalid handle for any error when throwOnError is false Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Diagnostics/ProcessManager.Windows.cs | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs index 8e96c30e065b60..102ba0be16367d 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs @@ -236,7 +236,7 @@ static unsafe ProcessManager() } } - public static SafeProcessHandle OpenProcess(int processId, int access, bool throwIfInaccessible) + public static SafeProcessHandle OpenProcess(int processId, int access, bool throwOnError) { SafeProcessHandle processHandle = Interop.Kernel32.OpenProcess(access, false, processId); int result = Marshal.GetLastWin32Error(); @@ -247,46 +247,24 @@ public static SafeProcessHandle OpenProcess(int processId, int access, bool thro processHandle.Dispose(); + if (!throwOnError) + { + return SafeProcessHandle.InvalidHandle; + } + if (processId == 0) { throw new Win32Exception(Interop.Errors.ERROR_ACCESS_DENIED); } - // Handle different error conditions based on throwIfInaccessible parameter. - // When throwIfInaccessible is false (e.g., during process enumeration), return invalid handle - // for common/expected errors instead of throwing to avoid excessive exception overhead. - if (IsProcessAccessError(result)) + if (!IsProcessRunning(processId)) { - if (!throwIfInaccessible) - { - return SafeProcessHandle.InvalidHandle; - } - } - else if (!IsProcessRunning(processId)) - { - if (throwIfInaccessible) - { - throw new InvalidOperationException(SR.Format(SR.ProcessHasExited, processId.ToString())); - } - else - { - return SafeProcessHandle.InvalidHandle; - } + throw new InvalidOperationException(SR.Format(SR.ProcessHasExited, processId.ToString())); } throw new Win32Exception(result); } - private static bool IsProcessAccessError(int errorCode) - { - // Common errors when trying to access processes we don't have permission to query - return errorCode == Interop.Errors.ERROR_ACCESS_DENIED || - errorCode == Interop.Errors.ERROR_INVALID_ACCESS || - errorCode == Interop.Errors.ERROR_NETWORK_ACCESS_DENIED || - errorCode == Interop.Errors.ERROR_NO_SUCH_PRIVILEGE || - errorCode == Interop.Errors.ERROR_PRIVILEGE_NOT_HELD; - } - public static SafeThreadHandle OpenThread(int threadId, int access) { SafeThreadHandle threadHandle = Interop.Kernel32.OpenThread(access, false, threadId); From 74858384aea80be2366d43994beff81027777b40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 05:41:36 +0000 Subject: [PATCH 8/9] Add non-throwing TryGetStartTime and TryGetParentProcessId helpers to avoid first-chance exceptions during enumeration Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/Diagnostics/Process.Win32.cs | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs index 95703701791797..059204fe7a9780 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs @@ -328,13 +328,65 @@ private bool WaitForInputIdleCore(int milliseconds) /// private bool IsParentOf(Process possibleChild) { - try + // Use non-throwing helpers to avoid first-chance exceptions during enumeration. + // This is critical for performance when a debugger is attached. + if (!TryGetStartTime(out DateTime myStartTime) || + !possibleChild.TryGetStartTime(out DateTime childStartTime) || + !possibleChild.TryGetParentProcessId(out int childParentId)) { - return StartTime < possibleChild.StartTime && Id == possibleChild.ParentProcessId; + return false; } - catch (Exception e) when (IsProcessInvalidException(e)) + + return myStartTime < childStartTime && Id == childParentId; + } + + /// + /// Try to get the process start time without throwing exceptions. + /// + private bool TryGetStartTime(out DateTime startTime) + { + startTime = default; + using (SafeProcessHandle handle = GetProcessHandle(Interop.Advapi32.ProcessOptions.PROCESS_QUERY_LIMITED_INFORMATION, false)) { - return false; + if (handle.IsInvalid) + { + return false; + } + + ProcessThreadTimes processTimes = new ProcessThreadTimes(); + if (!Interop.Kernel32.GetProcessTimes(handle, + out processTimes._create, out processTimes._exit, + out processTimes._kernel, out processTimes._user)) + { + return false; + } + + startTime = processTimes.StartTime; + return true; + } + } + + /// + /// Try to get the parent process ID without throwing exceptions. + /// + private unsafe bool TryGetParentProcessId(out int parentProcessId) + { + parentProcessId = 0; + using (SafeProcessHandle handle = GetProcessHandle(Interop.Advapi32.ProcessOptions.PROCESS_QUERY_LIMITED_INFORMATION, false)) + { + if (handle.IsInvalid) + { + return false; + } + + Interop.NtDll.PROCESS_BASIC_INFORMATION info; + if (Interop.NtDll.NtQueryInformationProcess(handle, Interop.NtDll.ProcessBasicInformation, &info, (uint)sizeof(Interop.NtDll.PROCESS_BASIC_INFORMATION), out _) != 0) + { + return false; + } + + parentProcessId = (int)info.InheritedFromUniqueProcessId; + return true; } } @@ -359,14 +411,14 @@ private unsafe int ParentProcessId private bool Equals(Process process) { - try - { - return Id == process.Id && StartTime == process.StartTime; - } - catch (Exception e) when (IsProcessInvalidException(e)) + // Use non-throwing helper to avoid first-chance exceptions during enumeration. + if (!TryGetStartTime(out DateTime myStartTime) || + !process.TryGetStartTime(out DateTime otherStartTime)) { return false; } + + return Id == process.Id && myStartTime == otherStartTime; } private List? KillTree() From 2d29122e6b7c53445374fb785b8196472bf9417c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:45:02 +0000 Subject: [PATCH 9/9] Optimize Equals to check process ID before gathering start times Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/Diagnostics/Process.Win32.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs index 059204fe7a9780..bd551a53f8d311 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs @@ -411,6 +411,12 @@ private unsafe int ParentProcessId private bool Equals(Process process) { + // Check IDs first since they're cheap to compare and will fail most of the time. + if (Id != process.Id) + { + return false; + } + // Use non-throwing helper to avoid first-chance exceptions during enumeration. if (!TryGetStartTime(out DateTime myStartTime) || !process.TryGetStartTime(out DateTime otherStartTime)) @@ -418,7 +424,7 @@ private bool Equals(Process process) return false; } - return Id == process.Id && myStartTime == otherStartTime; + return myStartTime == otherStartTime; } private List? KillTree()