From 362d6410af34a9e18ce4253eaeb16e3e70d82af9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 10:06:49 +0000
Subject: [PATCH 1/4] Enable StartSuspended on all Unix platforms
- Remove [SupportedOSPlatform] attributes from StartSuspended and Resume()
- Remove platform check in SafeProcessHandle.Resume() and ProcessStartInfo validation
- In native pal_process.c: remove ENOTSUP check, use fork() instead of vfork()
when startSuspended is true, and raise(SIGSTOP) before execve
- Close the exec-waiting pipe before SIGSTOP so parent can proceed
- Update tests: remove PlatformSpecific attribute and non-supported test class
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../ref/System.Diagnostics.Process.cs | 4 --
.../Win32/SafeHandles/SafeProcessHandle.cs | 10 +----
.../System/Diagnostics/ProcessStartInfo.cs | 12 ++----
.../tests/StartSuspendedTests.cs | 12 ------
src/native/libs/System.Native/pal_process.c | 39 ++++++++++++++-----
5 files changed, 34 insertions(+), 43 deletions(-)
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 52dfab59a18f05..552e7367026faa 100644
--- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
+++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
@@ -16,8 +16,6 @@ public SafeProcessHandle(System.IntPtr existingHandle, bool ownsHandle) : base (
public void Kill() { }
public int ProcessId { get { throw null; } }
protected override bool ReleaseHandle() { throw null; }
- [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
- [System.Runtime.Versioning.SupportedOSPlatformAttribute("macos")]
public void Resume() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
@@ -356,8 +354,6 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
public System.Text.Encoding? StandardOutputEncoding { get { throw null; } set { } }
public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardOutputHandle { get { throw null; } set { } }
public bool StartDetached { get { throw null; } set { } }
- [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
- [System.Runtime.Versioning.SupportedOSPlatformAttribute("macos")]
public bool StartSuspended { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool UseCredentialsForNetworkingOnly { get { throw null; } set { } }
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
index ad3ade2fa5b5b3..2f7fdacebcf0e3 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
@@ -210,20 +210,12 @@ internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool fallbac
///
///
/// On Windows, this calls ResumeThread on the main thread of the process.
- /// On macOS, this sends SIGCONT to the process.
+ /// On Unix, this sends SIGCONT to the process.
///
/// The handle is invalid.
- /// The current operating system is not supported.
/// The OS call to resume the process failed.
- [SupportedOSPlatform("windows")]
- [SupportedOSPlatform("macos")]
public void Resume()
{
- if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
- {
- throw new PlatformNotSupportedException();
- }
-
Validate();
ResumeCore();
}
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
index 96009d99e1054a..62053cb9856d96 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
@@ -161,11 +161,12 @@ public string Arguments
/// On macOS, the process is started with the POSIX_SPAWN_START_SUSPENDED flag.
///
///
+ /// On other Unix platforms, the child process raises SIGSTOP against itself before calling execve.
+ ///
+ ///
/// This property cannot be used together with set to .
///
///
- [SupportedOSPlatform("windows")]
- [SupportedOSPlatform("macos")]
public bool StartSuspended { get; set; }
///
@@ -473,17 +474,10 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite
throw new InvalidOperationException(SR.StartDetachedNotCompatible);
}
-#pragma warning disable CA1416 // StartSuspended getter works on all platforms; the attribute guards the actual effect
- if (StartSuspended && !OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
- {
- throw new PlatformNotSupportedException();
- }
-
if (StartSuspended && UseShellExecute)
{
throw new InvalidOperationException(SR.StartSuspendedNotCompatible);
}
-#pragma warning restore CA1416
if (InheritedHandles is not null && (UseShellExecute || !string.IsNullOrEmpty(UserName)))
{
diff --git a/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs b/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
index d748ed1b272370..e3b1d979101d06 100644
--- a/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
@@ -12,7 +12,6 @@
namespace System.Diagnostics.Tests
{
[ConditionalClass(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)]
public class StartSuspendedTests : ProcessTestBase
{
[ConditionalFact]
@@ -176,15 +175,4 @@ public async Task StartSuspended_WithPipeRedirection_Works()
}
}
}
-
- public class StartSuspendedTests_NonWindowsNonMacOS : ProcessTestBase
- {
- [Fact]
- [SkipOnPlatform(TestPlatforms.Windows | TestPlatforms.OSX | TestPlatforms.iOS | TestPlatforms.tvOS, "Resume is supported on Windows and macOS, and SafeProcessHandle.Open is not supported on iOS and tvOS")]
- public void Resume_OnNonSupportedOS_ThrowsPlatformNotSupportedException()
- {
- using SafeProcessHandle handle = SafeProcessHandle.Open(Environment.ProcessId);
- Assert.Throws(() => handle.Resume());
- }
- }
}
diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c
index c5af90e397287e..90135b434e1313 100644
--- a/src/native/libs/System.Native/pal_process.c
+++ b/src/native/libs/System.Native/pal_process.c
@@ -729,13 +729,6 @@ static int32_t ForkAndExecProcessInternal(
#endif
#if HAVE_FORK
- if (startSuspended)
- {
- // POSIX_SPAWN_START_SUSPENDED is only available in the posix_spawn() path (macOS, !setCredentials).
- // The fork() path does not support startSuspended.
- errno = ENOTSUP;
- return -1;
- }
bool success = true;
int waitForChildToExecPipe[2] = {-1, -1};
pid_t processId = -1;
@@ -811,11 +804,23 @@ static int32_t ForkAndExecProcessInternal(
// on another thread while a vfork() child is still pending, bad things are possible; however we
// do not do that.
+ // When startSuspended is true, we must use fork() instead of vfork() because we need to
+ // stop the child process before calling execve(), which is not safe with vfork() (the parent
+ // is blocked until the child calls execve or exits).
+
#if defined (__GLIBC__)
- if ((processId = vfork()) == 0) // processId == 0 if this is child process
+ if (startSuspended)
+ {
+ processId = fork();
+ }
+ else
+ {
+ processId = vfork();
+ }
+ if (processId == 0)
#else
// musl libc has an undocumented failure mode around setuid(); we must exclude it.
- if (setCredentials)
+ if (setCredentials || startSuspended)
{
processId = fork();
}
@@ -933,6 +938,22 @@ static int32_t ForkAndExecProcessInternal(
}
#endif
+ // If startSuspended is requested, stop the child process before exec.
+ // The parent will send SIGCONT (via Resume()) when ready.
+ if (startSuspended)
+ {
+ // Close the write end of the exec-waiting pipe before stopping.
+ // This signals to the parent that the child has been set up successfully
+ // (no error written to the pipe), allowing the parent to return the child PID.
+ // When the child is later resumed with SIGCONT, it will proceed to execve.
+ close(waitForChildToExecPipe[WRITE_END_OF_PIPE]);
+ waitForChildToExecPipe[WRITE_END_OF_PIPE] = -1;
+
+ // Send SIGSTOP to the current process. raise() sends the signal to the
+ // calling thread. This is safe because after fork the child is single-threaded.
+ raise(SIGSTOP);
+ }
+
// Finally, execute the new process. execve will not return if it's successful.
execve(filename, argv, envp);
ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); // execve failed
From 20a68182467c5171614058aad3e0654ed32a2150 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 12:03:30 +0000
Subject: [PATCH 2/4] Address reviewer feedback: waitpid(WUNTRACED) sync, doc
clarification, immediate-resume test
- pal_process.c: add waitpid(WUNTRACED) in parent after pipe close for
startSuspended to eliminate SIGCONT/SIGSTOP race condition
- ProcessStartInfo.cs: clarify in docs that on non-macOS Unix suspension
occurs before execve (process image not yet replaced)
- StartSuspendedTests.cs: add StartSuspended_ResumeImmediately_Succeeds
test that starts suspended and immediately calls Resume
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System/Diagnostics/ProcessStartInfo.cs | 3 ++-
.../tests/StartSuspendedTests.cs | 13 +++++++++++
src/native/libs/System.Native/pal_process.c | 22 +++++++++++++++++++
3 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
index 62053cb9856d96..081e1bbe60a94a 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
@@ -161,7 +161,8 @@ public string Arguments
/// On macOS, the process is started with the POSIX_SPAWN_START_SUSPENDED flag.
///
///
- /// On other Unix platforms, the child process raises SIGSTOP against itself before calling execve.
+ /// On other Unix platforms, the child process raises SIGSTOP against itself before calling execve,
+ /// so the process has not yet transitioned to the target program image when it is suspended.
///
///
/// This property cannot be used together with set to .
diff --git a/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs b/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
index e3b1d979101d06..61dc300c2fe421 100644
--- a/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
@@ -14,6 +14,19 @@ namespace System.Diagnostics.Tests
[ConditionalClass(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public class StartSuspendedTests : ProcessTestBase
{
+ [ConditionalFact]
+ public void StartSuspended_ResumeImmediately_Succeeds()
+ {
+ Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode);
+ process.StartInfo.StartSuspended = true;
+
+ using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo);
+ processHandle.Resume();
+
+ ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS));
+ Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode);
+ }
+
[ConditionalFact]
public void StartSuspended_ResumeCompletes()
{
diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c
index 90135b434e1313..9dccfa96ef3efb 100644
--- a/src/native/libs/System.Native/pal_process.c
+++ b/src/native/libs/System.Native/pal_process.c
@@ -995,6 +995,28 @@ done:;
CloseIfOpen(waitForChildToExecPipe[READ_END_OF_PIPE]);
}
+ // When startSuspended is requested, the child closes the exec-waiting pipe just before
+ // calling raise(SIGSTOP). Wait here until the child is truly in the stopped state so that
+ // the caller cannot send SIGCONT (via Resume()) before the child has delivered SIGSTOP.
+ // Without this synchronization, SIGCONT may arrive before SIGSTOP, causing the child to
+ // stop permanently on the subsequent SIGSTOP.
+ if (startSuspended && success && processId > 0)
+ {
+ int childStatus;
+ pid_t waitResult;
+ do
+ {
+ waitResult = waitpid(processId, &childStatus, WUNTRACED);
+ }
+ while (waitResult == -1 && errno == EINTR);
+
+ if (waitResult == -1 || !WIFSTOPPED(childStatus))
+ {
+ success = false;
+ priorErrno = (waitResult == -1) ? errno : ECHILD;
+ }
+ }
+
// If we failed, give back error values in all out arguments.
if (!success)
{
From edf0452aab612713b86407d0bd53198371eaee7e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 12:06:23 +0000
Subject: [PATCH 3/4] Refine waitpid error handling in startSuspended path
- Remove redundant processId > 0 guard (success implies positive PID)
- Separate WIFEXITED/WIFSIGNALED cases with clearer ECHILD error and comment
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
src/native/libs/System.Native/pal_process.c | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c
index 9dccfa96ef3efb..6ec871286145ea 100644
--- a/src/native/libs/System.Native/pal_process.c
+++ b/src/native/libs/System.Native/pal_process.c
@@ -1000,7 +1000,7 @@ done:;
// the caller cannot send SIGCONT (via Resume()) before the child has delivered SIGSTOP.
// Without this synchronization, SIGCONT may arrive before SIGSTOP, causing the child to
// stop permanently on the subsequent SIGSTOP.
- if (startSuspended && success && processId > 0)
+ if (startSuspended && success)
{
int childStatus;
pid_t waitResult;
@@ -1010,10 +1010,16 @@ done:;
}
while (waitResult == -1 && errno == EINTR);
- if (waitResult == -1 || !WIFSTOPPED(childStatus))
+ if (waitResult == -1)
{
success = false;
- priorErrno = (waitResult == -1) ? errno : ECHILD;
+ priorErrno = errno;
+ }
+ else if (!WIFSTOPPED(childStatus))
+ {
+ // Child exited (WIFEXITED) or was killed by a signal (WIFSIGNALED) before stopping.
+ success = false;
+ priorErrno = ECHILD;
}
}
From 15443b55531a519a3401bf0e28be693b7f58933f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 14:09:06 +0000
Subject: [PATCH 4/4] Apply using disposal to Process in
StartSuspended_ResumeImmediately_Succeeds test
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.Diagnostics.Process/tests/StartSuspendedTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs b/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
index 61dc300c2fe421..7d134824c76ac3 100644
--- a/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/StartSuspendedTests.cs
@@ -17,7 +17,7 @@ public class StartSuspendedTests : ProcessTestBase
[ConditionalFact]
public void StartSuspended_ResumeImmediately_Succeeds()
{
- Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode);
+ using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode);
process.StartInfo.StartSuspended = true;
using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo);