Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f47b2fd
Refactor StartCore to accept SafeFileHandle parameters; move pipe cre…
Copilot Mar 18, 2026
fea2a80
Fix build errors: XML doc comments and unsafe partial mismatch; add I…
Copilot Mar 18, 2026
07d9369
Address PR feedback: remove partial methods, use SafeFileHandle.Creat…
Copilot Mar 19, 2026
b65842b
address my own feedback
adamsitnik Mar 19, 2026
43b6301
more polishing after reading the code again
adamsitnik Mar 19, 2026
dc85b03
fix Unix build
adamsitnik Mar 19, 2026
9ee1881
trigger the CI as it seems to got stuck
adamsitnik Mar 19, 2026
17bc483
fix the tests:
adamsitnik Mar 19, 2026
c9c55f0
Merge branch 'main' into copilot/refactor-startcore-method-arguments
adamsitnik Mar 19, 2026
7cf7f0c
handle INVALID_HANDLE_VALUE on Windows
adamsitnik Mar 19, 2026
2c8c444
address code review feedback: don't use Console.OpenStandard*Handle o…
adamsitnik Mar 19, 2026
bba0144
Make Console.OpenStandard*Handle APIs work on Android by returning fd…
Copilot Mar 19, 2026
7a345e2
Revert "Make Console.OpenStandard*Handle APIs work on Android by retu…
adamsitnik Mar 20, 2026
b362a72
address code review feedback: don't dup 0/1/2 on Unix
adamsitnik Mar 20, 2026
419d7ab
Add StandardInput/Output/Error SafeFileHandle properties to ProcessSt…
Copilot Mar 20, 2026
18995de
Remove unrelated files from commit
Copilot Mar 20, 2026
d40cf18
Address code review: fix pipe handle cleanup in tests
Copilot Mar 20, 2026
60844f3
Address review feedback: reuse CantRedirectStreams, add LeaveHandlesO…
Copilot Mar 20, 2026
c8b8723
Address feedback: handles don't need to be inheritable, mention OpenN…
Copilot Mar 20, 2026
12edafb
Merge branch 'main' into copilot/refactor-startcore-method-arguments
adamsitnik Mar 21, 2026
6718589
Only create pipe handles for redirected streams, update callers to ha…
Copilot Mar 21, 2026
6ddf417
Fix usesTerminal logic: null handle means child inherits parent's str…
Copilot Mar 21, 2026
dfec029
Merge branch 'main' into copilot/refactor-startcore-method-arguments
adamsitnik Mar 22, 2026
4199424
Merge branch 'main' into copilot/refactor-startcore-method-arguments
adamsitnik Mar 23, 2026
0aa6fcf
Merge branch 'copilot/refactor-startcore-method-arguments' into copil…
adamsitnik Mar 23, 2026
017f286
Address review feedback: simplify test try/catch, remove manual Close…
Copilot Mar 23, 2026
95247af
address my own feedback:
adamsitnik Mar 23, 2026
7920b52
Rename StandardInput/Output/Error to StandardInputHandle/OutputHandle…
Copilot Mar 23, 2026
477224c
don't duplicate the handle if it's inheritable already
adamsitnik Mar 24, 2026
a914e57
remove LeaveHandlesOpen
adamsitnik Mar 24, 2026
7c1fa73
improve wording
adamsitnik Mar 24, 2026
c0ecbbc
address code review feedback
adamsitnik Mar 24, 2026
e30abcc
Apply suggestions from code review
adamsitnik Mar 24, 2026
e81296d
Fix ValidateHandle order and use fully qualified XML doc cref names
Copilot Mar 24, 2026
bb618c3
Switch to posix_spawn on Apple targets in SystemNative_ForkAndExecPro…
Copilot Mar 24, 2026
0c0e7ca
Add POSIX_SPAWN_SETSIGMASK to set clean signal mask for spawned child
Copilot Mar 24, 2026
1f39d43
Address PR feedback: use TARGET_OSX guard, set childPid early, captur…
Copilot Mar 25, 2026
88b810a
Fix test: use RemotelyInvokable.SuccessExitCode (42) instead of 0
Copilot Mar 25, 2026
2020c48
Add SkipOnPlatform attribute to restrict signal mask test to supporte…
Copilot Mar 25, 2026
a9551ed
Fix SkipOnPlatform to skip Windows instead of non-Unix mobile platforms
Copilot Mar 25, 2026
90f070d
Fix signal-default setup: only reset custom handlers, skip SIGKILL/SI…
Copilot Mar 25, 2026
b5c490d
Add missing #include <spawn.h> for posix_spawn on macOS
Copilot Mar 25, 2026
5bda57f
Merge branch 'main' into copilot/switch-to-posix-spawn-apple-targets
adamsitnik Mar 26, 2026
a9b574e
Reduce code duplication: combine setsigdefault, pthread_sigmask, sets…
Copilot Mar 26, 2026
835a976
reduce code duplication, address feedback
adamsitnik Mar 26, 2026
cfc8713
Fix build error: move current_mask declaration before its use in comp…
Copilot Mar 26, 2026
729fd3b
Revert "Fix build error: move current_mask declaration before its use…
Copilot Mar 26, 2026
c0afd23
Revert compound conditional change, just move current_mask declaratio…
Copilot Mar 26, 2026
3407e5e
Merge branch 'main' into copilot/switch-to-posix-spawn-apple-targets
stephentoub Mar 26, 2026
54f7706
Fix nit: add blank line before struct sigaction, fix double-comment typo
Copilot Mar 26, 2026
c878374
Apply suggestions from code review
adamsitnik Mar 26, 2026
7e8c985
Merge branch 'main' into copilot/switch-to-posix-spawn-apple-targets
adamsitnik Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1072,5 +1072,54 @@ private static void SendSignal(PosixSignal signal, int processId)
}

private static unsafe void ReEnableCtrlCHandlerIfNeeded(PosixSignal signal) { }

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[SkipOnPlatform(TestPlatforms.Windows, "SIGCONT is not supported on Windows.")]
public void ChildProcess_WithParentSignalHandler_CanReceiveSignals()
{
// This test verifies that a child process started from a parent that has
// registered signal handlers can still receive signals correctly.
// This exercises the posix_spawn path on macOS where the child must
// cooperate correctly with signal handling in both the parent and child.
const string SignalReceivedMessage = "Signal received";

using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(() =>
{
// Register a signal handler in the parent process to modify signal state
using PosixSignalRegistration parentHandler = PosixSignalRegistration.Create(PosixSignal.SIGCONT, (ctx) =>
{
ctx.Cancel = true;
});

// Now start a child process from this parent (which has signal handlers registered)
// and verify the child can receive signals properly
const string ChildReadyMessage = "Child ready";

var childOptions = new RemoteInvokeOptions { CheckExitCode = false };
childOptions.StartInfo.RedirectStandardOutput = true;

using RemoteInvokeHandle childHandle = RemoteExecutor.Invoke(() =>
{
using ManualResetEvent signalEvent = new ManualResetEvent(false);
using PosixSignalRegistration childHandler = PosixSignalRegistration.Create(PosixSignal.SIGCONT, (ctx) =>
{
Console.WriteLine(SignalReceivedMessage);
signalEvent.Set();
ctx.Cancel = true;
});

Console.WriteLine(ChildReadyMessage);
Assert.True(signalEvent.WaitOne(WaitInMS));
}, childOptions);

AssertRemoteProcessStandardOutputLine(childHandle, ChildReadyMessage, WaitInMS);

// Send SIGCONT to the child process
SendSignal(PosixSignal.SIGCONT, childHandle.Process.Id);

Assert.True(childHandle.Process.WaitForExit(WaitInMS));
Assert.Equal(RemotelyInvokable.SuccessExitCode, childHandle.Process.ExitCode);
});
}
}
}
125 changes: 111 additions & 14 deletions src/native/libs/System.Native/pal_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

#ifdef __APPLE__
#include <mach-o/dyld.h>
#include <spawn.h>
Comment thread
adamsitnik marked this conversation as resolved.
#endif

#ifdef __FreeBSD__
Expand Down Expand Up @@ -219,6 +220,116 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
int32_t stdoutFd,
int32_t stderrFd)
{
#if HAVE_FORK || defined(TARGET_OSX)
Comment thread
adamsitnik marked this conversation as resolved.
assert(NULL != filename && NULL != argv && NULL != envp && NULL != childPid &&
Comment thread
adamsitnik marked this conversation as resolved.
(groupsLength == 0 || groups != NULL) && "null argument.");

*childPid = -1;

// Make sure we can find and access the executable. exec will do this, of course, but at that point it's already
// in the child process, at which point it'll translate to the child process' exit code rather than to failing
// the Start itself. There's a race condition here, in that this could change prior to exec's checks, but there's
// little we can do about that. There are also more rigorous checks exec does, such as validating the executable
// format of the target; such errors will emerge via the child process' exit code.
if (access(filename, X_OK) != 0)
{
return -1;
}
#endif

#if defined(TARGET_OSX)
// Use posix_spawn on macOS when credentials don't need to be set,
// since macOS does not support setuid/setgid with posix_spawn.
if (!setCredentials)
{
pid_t spawnedPid;
posix_spawn_file_actions_t file_actions;
posix_spawnattr_t attr;
int result;

if ((result = posix_spawnattr_init(&attr)) != 0)
{
errno = result;
return -1;
}

// Build sigdefault set: only reset signals that have custom handlers,
// preserving SIG_IGN and SIG_DFL handlers (matching fork path behavior).
sigset_t sigdefault_set;
sigemptyset(&sigdefault_set);
for (int sig = 1; sig < NSIG; ++sig)
{
if (sig == SIGKILL || sig == SIGSTOP)
{
continue;
}

struct sigaction sa_old;
if (!sigaction(sig, NULL, &sa_old))
{
void (*oldhandler)(int) = handler_from_sigaction(&sa_old);
if (oldhandler != SIG_IGN && oldhandler != SIG_DFL)
{
sigaddset(&sigdefault_set, sig);
}
}
}

// pthread_sigmask follows POSIX thread conventions: it returns an error number but does not set errno
sigset_t current_mask;
result = pthread_sigmask(SIG_SETMASK, NULL, &current_mask);
if (result != 0)
{
posix_spawnattr_destroy(&attr);
errno = result;
return -1;
}

// POSIX_SPAWN_SETSIGDEF to reset signal handlers to default
// POSIX_SPAWN_SETSIGMASK to set the child's signal mask
short flags = POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK;

if ((result = posix_spawnattr_setflags(&attr, flags)) != 0
|| (result = posix_spawnattr_setsigdefault(&attr, &sigdefault_set)) != 0
|| (result = posix_spawnattr_setsigmask(&attr, &current_mask)) != 0 // Set the child's signal mask to match the parent's current mask
|| (result = posix_spawn_file_actions_init(&file_actions)) != 0)
{
int saved_errno = result;
posix_spawnattr_destroy(&attr);
errno = saved_errno;
return -1;
}

// Redirect stdin/stdout/stderr
if ((stdinFd != -1 && (result = posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO)) != 0)
|| (stdoutFd != -1 && (result = posix_spawn_file_actions_adddup2(&file_actions, stdoutFd, STDOUT_FILENO)) != 0)
|| (stderrFd != -1 && (result = posix_spawn_file_actions_adddup2(&file_actions, stderrFd, STDERR_FILENO)) != 0)
|| (cwd != NULL && (result = posix_spawn_file_actions_addchdir_np(&file_actions, cwd)) != 0)) // Change working directory if specified
{
int saved_errno = result;
posix_spawn_file_actions_destroy(&file_actions);
posix_spawnattr_destroy(&attr);
errno = saved_errno;
return -1;
}

// Spawn the process
result = posix_spawn(&spawnedPid, filename, &file_actions, &attr, argv, envp);

posix_spawn_file_actions_destroy(&file_actions);
posix_spawnattr_destroy(&attr);

if (result != 0)
{
errno = result;
return -1;
}

*childPid = spawnedPid;
return 0;
}
#endif

#if HAVE_FORK
bool success = true;
int waitForChildToExecPipe[2] = {-1, -1};
Expand All @@ -234,9 +345,6 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &thread_cancel_state);
#endif

assert(NULL != filename && NULL != argv && NULL != envp && NULL != childPid &&
(groupsLength == 0 || groups != NULL) && "null argument.");

if (setCredentials && groupsLength > 0)
{
getGroupsBuffer = (uint32_t*)(malloc(sizeof(uint32_t) * Int32ToSizeT(groupsLength)));
Expand All @@ -247,17 +355,6 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
}
}

// Make sure we can find and access the executable. exec will do this, of course, but at that point it's already
// in the child process, at which point it'll translate to the child process' exit code rather than to failing
// the Start itself. There's a race condition here, in that this could change prior to exec's checks, but there's
// little we can do about that. There are also more rigorous checks exec does, such as validating the executable
// format of the target; such errors will emerge via the child process' exit code.
if (access(filename, X_OK) != 0)
{
success = false;
goto done;
}

// We create a pipe purely for the benefit of knowing when the child process has called exec.
// We can use that to block waiting on the pipe to be closed, which lets us block the parent
// from returning until the child process is actually transitioned to the target program. This
Expand Down
Loading