diff --git a/libc/intrin/sig.c b/libc/intrin/sig.c index 3303a8378..4dd2f75e4 100644 --- a/libc/intrin/sig.c +++ b/libc/intrin/sig.c @@ -667,6 +667,9 @@ textwindows int __sig_check(void) { return res; } +// this mutex is needed so execve() can shut down the signal worker +pthread_mutex_t __sig_worker_lock; + // background thread for delivering inter-process signals asynchronously // this checks for undelivered process-wide signals, once per scheduling // quantum, which on windows should be every ~15ms or so, unless somehow @@ -680,6 +683,7 @@ textwindows dontinstrument static uint32_t __sig_worker(void *arg) { __maps_track((char *)(((uintptr_t)sp + __pagesize - 1) & -__pagesize) - STKSZ, STKSZ); for (;;) { + pthread_mutex_lock(&__sig_worker_lock); // dequeue all pending signals and fire them off. if there's no // thread that can handle them then __sig_generate will requeue @@ -724,6 +728,7 @@ textwindows dontinstrument static uint32_t __sig_worker(void *arg) { _pthread_unlock(); // wait until next scheduler quantum + pthread_mutex_unlock(&__sig_worker_lock); Sleep(POLL_INTERVAL_MS); } return 0; diff --git a/libc/intrin/terminatethisprocess.c b/libc/intrin/terminatethisprocess.c index d036c4f31..2f61cdb27 100644 --- a/libc/intrin/terminatethisprocess.c +++ b/libc/intrin/terminatethisprocess.c @@ -18,7 +18,6 @@ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/atomic.h" #include "libc/calls/sig.internal.h" -#include "libc/intrin/kprintf.h" #include "libc/limits.h" #include "libc/nt/files.h" #include "libc/nt/memory.h" diff --git a/libc/proc/execve-nt.greg.c b/libc/proc/execve-nt.greg.c index fbb13bd46..bd514b4ff 100644 --- a/libc/proc/execve-nt.greg.c +++ b/libc/proc/execve-nt.greg.c @@ -17,13 +17,14 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/assert.h" +#include "libc/calls/calls.h" #include "libc/calls/internal.h" +#include "libc/calls/sig.internal.h" #include "libc/calls/struct/sigset.internal.h" #include "libc/calls/syscall-nt.internal.h" #include "libc/errno.h" #include "libc/fmt/itoa.h" #include "libc/intrin/fds.h" -#include "libc/intrin/kprintf.h" #include "libc/mem/mem.h" #include "libc/nt/enum/processaccess.h" #include "libc/nt/enum/startf.h" @@ -33,8 +34,10 @@ #include "libc/nt/runtime.h" #include "libc/nt/struct/processinformation.h" #include "libc/nt/struct/startupinfo.h" +#include "libc/nt/thunk/msabi.h" #include "libc/proc/describefds.internal.h" #include "libc/proc/ntspawn.h" +#include "libc/runtime/internal.h" #include "libc/str/str.h" #include "libc/sysv/consts/at.h" #include "libc/sysv/consts/o.h" @@ -43,23 +46,37 @@ #include "libc/thread/thread.h" #ifdef __x86_64__ +__msabi extern typeof(TerminateProcess) *const __imp_TerminateProcess; + +extern pthread_mutex_t __sig_worker_lock; + +static void sys_execve_nt_abort(sigset_t sigmask) { + _pthread_unlock(); + pthread_mutex_unlock(&__sig_worker_lock); + __sig_unblock(sigmask); +} + textwindows int sys_execve_nt(const char *program, char *const argv[], char *const envp[]) { // execve() needs to be @asyncsignalsafe sigset_t sigmask = __sig_block(); - _pthread_lock(); + pthread_mutex_lock(&__sig_worker_lock); // order matters + _pthread_lock(); // order matters // new process should be a child of our parent int64_t hParentProcess; int ppid = sys_getppid_nt(); if (!(hParentProcess = OpenProcess( kNtProcessDupHandle | kNtProcessCreateProcess, false, ppid))) { - _pthread_unlock(); - __sig_unblock(sigmask); + sys_execve_nt_abort(sigmask); return -1; } + // inherit pid + char pidvar[11 + 21]; + FormatUint64(stpcpy(pidvar, "_COSMO_PID="), __pid); + // inherit signal mask char maskvar[6 + 21]; FormatUint64(stpcpy(maskvar, "_MASK="), sigmask); @@ -84,22 +101,26 @@ textwindows int sys_execve_nt(const char *program, char *const argv[], if (!(fdspec = __describe_fds(g_fds.p, g_fds.n, &si, hParentProcess, &lpExplicitHandles, &dwExplicitHandleCount))) { CloseHandle(hParentProcess); - _pthread_unlock(); - __sig_unblock(sigmask); + sys_execve_nt_abort(sigmask); return -1; } + // inherit pending signals + atomic_fetch_or_explicit( + __sig.process, + atomic_load_explicit(&__get_tls()->tib_sigpending, memory_order_acquire), + memory_order_release); + // launch the process struct NtProcessInformation pi; int rc = ntspawn(&(struct NtSpawnArgs){ - AT_FDCWD, program, argv, envp, (char *[]){fdspec, maskvar, 0}, 0, 0, - hParentProcess, lpExplicitHandles, dwExplicitHandleCount, &si, &pi}); + AT_FDCWD, program, argv, envp, (char *[]){fdspec, maskvar, pidvar, 0}, 0, + 0, hParentProcess, lpExplicitHandles, dwExplicitHandleCount, &si, &pi}); __undescribe_fds(hParentProcess, lpExplicitHandles, dwExplicitHandleCount); if (rc == -1) { free(fdspec); CloseHandle(hParentProcess); - _pthread_unlock(); - __sig_unblock(sigmask); + sys_execve_nt_abort(sigmask); if (GetLastError() == kNtErrorSharingViolation) { return etxtbsy(); } else { @@ -112,12 +133,13 @@ textwindows int sys_execve_nt(const char *program, char *const argv[], if (DuplicateHandle(GetCurrentProcess(), pi.hProcess, hParentProcess, &handle, 0, false, kNtDuplicateSameAccess)) { unassert(!(handle & 0xFFFFFFFFFF000000)); - TerminateThisProcess(0x23000000u | handle); + __imp_TerminateProcess(-1, 0x23000000u | handle); } else { // TODO(jart): Why does `make loc` print this? // kprintf("DuplicateHandle failed w/ %d\n", GetLastError()); - TerminateThisProcess(ECHILD); + __imp_TerminateProcess(-1, ECHILD); } + __builtin_unreachable(); } #endif /* __x86_64__ */ diff --git a/libc/proc/execve.c b/libc/proc/execve.c index 781bd3f26..b610f8b29 100644 --- a/libc/proc/execve.c +++ b/libc/proc/execve.c @@ -36,14 +36,55 @@ /** * Replaces current process with program. * + * Your `prog` may be an actually portable executable or a platform + * native binary (e.g. ELF, Mach-O, PE). On UNIX systems, your execve + * implementation will try to find where the `ape` interpreter program + * is installed on your system. The preferred location is `/usr/bin/ape` + * except on Apple Silicon where it's `/usr/local/bin/ape`. The $TMPDIR + * and $HOME locations that the APE shell script extracts the versioned + * ape binaries to will also be checked as a fallback path. Finally, if + * `prog` isn't an executable in any recognizable format, cosmo assumes + * it's a bourne shell script and launches it under /bin/sh. + * + * The signal mask and pending signals are inherited by the new process. + * Note the NetBSD kernel has a bug where pending signals are cleared. + * + * File descriptors that haven't been marked `O_CLOEXEC` through various + * devices such as open() and fcntl() will be inherited by the executed + * subprocess. The current file position of the duplicated descriptors + * is shared across processes. On Windows, `prog` needs to be built by + * cosmocc in order to properly inherit file descriptors. If a program + * compiled by MSVC or Cygwin is launched instead, then only the stdio + * file descriptors can be passed along. + * * On Windows, `argv` and `envp` can't contain binary strings. They need * to be valid UTF-8 in order to round-trip the WIN32 API, without being * corrupted. * - * On Windows, only file descriptors 0, 1 and 2 can be passed to a child - * process in such a way that allows them to be automatically discovered - * when the child process initializes. Cosmpolitan currently treats your - * other file descriptors as implicitly O_CLOEXEC. + * On Windows, cosmo execve uses parent spoofing to implement the UNIX + * behavior of replacing the current process. Since POSIX.1 also needs + * us to maintain the same PID number too, the _COSMO_PID environemnt + * variable is passed to the child process which specifies a spoofed + * PID. Whatever is in that variable will be reported by getpid() and + * other cosmo processes will be able to send signals to the process + * using that pid, via kill(). These synthetic PIDs which are only + * created by execve could potentially overlap with OS assignments if + * Windows recycles them. Cosmo avoids that by tracking handles of + * subprocesses. Each process has its own process manager thread, to + * associate pids with win32 handles, and execve will tell the parent + * process its new handle when it changes. However it's not perfect. + * There's still situations where processes created by execve() can + * cause surprising things to happen. For an alternative, consider + * posix_spawn() which is fastest and awesomest across all OSes. + * + * On Windows, support is currently not implemented for inheriting + * setitimer() and alarm() into an executed process. + * + * On Windows, support is currently not implemented for inheriting + * getrusage() statistics into an executed process. + * + * The executed process will share the same terminal and current + * directory. * * @param program will not be PATH searched, see commandv() * @param argv[0] is the name of the program to run diff --git a/libc/proc/kill-nt.c b/libc/proc/kill-nt.c index c2726fa81..c91bbe6b8 100644 --- a/libc/proc/kill-nt.c +++ b/libc/proc/kill-nt.c @@ -92,6 +92,7 @@ textwindows int sys_kill_nt(int pid, int sig) { int64_t handle, closeme = 0; if (!(handle = __proc_handle(pid))) { if ((handle = OpenProcess(kNtProcessTerminate, false, pid))) { + STRACE("warning: kill() using raw win32 pid"); closeme = handle; } else { goto OnError; @@ -103,7 +104,7 @@ textwindows int sys_kill_nt(int pid, int sig) { // now that we know the process exists, if it has a shared memory file // then we can be reasonably certain it's a cosmo process which should // be trusted to deliver its signal, unless it's a nine exterminations - if (pid > 0 && sig != 9) { + if (pid > 0) { atomic_ulong *sigproc; if ((sigproc = __sig_map_process(pid, kNtOpenExisting))) { if (sig > 0) @@ -112,12 +113,15 @@ textwindows int sys_kill_nt(int pid, int sig) { UnmapViewOfFile(sigproc); if (closeme) CloseHandle(closeme); - return 0; + if (sig != 9) + return 0; } } // perform actual kill // process will report WIFSIGNALED with WTERMSIG(sig) + if (sig != 9) + STRACE("warning: kill() sending %G via terminate", sig); bool32 ok = TerminateProcess(handle, sig); if (closeme) CloseHandle(closeme); diff --git a/libc/proc/kill.c b/libc/proc/kill.c index ec913d5c8..5de445fd3 100644 --- a/libc/proc/kill.c +++ b/libc/proc/kill.c @@ -35,6 +35,9 @@ * signal a cosmo process. The targeting process will then notice that a * signal has been added and delivers to any thread as soon as possible. * + * On Windows, the only signal that's guaranteed to work on non-cosmocc + * processes is SIGKILL. + * * On Windows, the concept of a process group isn't fully implemented. * Saying `kill(0, sig)` will deliver `sig` to all direct descendent * processes. Saying `kill(-pid, sig)` will be the same as saying diff --git a/libc/runtime/winmain.greg.c b/libc/runtime/winmain.greg.c index 0b53545ab..41fa5776d 100644 --- a/libc/runtime/winmain.greg.c +++ b/libc/runtime/winmain.greg.c @@ -300,6 +300,37 @@ static abi wontreturn void WinInit(const char16_t *cmdline) { (uintptr_t)(stackaddr + (stacksize - sizeof(struct WinArgs)))); } +static int Atoi(const char16_t *str) { + int c; + unsigned x = 0; + while ((c = *str++)) { + if ('0' <= c && c <= '9') { + x *= 10; + x += c - '0'; + } else { + return -1; + } + } + return x; +} + +static abi int WinGetPid(const char16_t *var, bool *out_is_inherited) { + uint32_t len; + char16_t val[12]; + if ((len = __imp_GetEnvironmentVariableW(var, val, ARRAYLEN(val)))) { + int pid = -1; + if (len < ARRAYLEN(val)) + pid = Atoi(val); + __imp_SetEnvironmentVariableW(var, NULL); + if (pid > 0) { + *out_is_inherited = true; + return pid; + } + } + *out_is_inherited = false; + return __imp_GetCurrentProcessId(); +} + abi int64_t WinMain(int64_t hInstance, int64_t hPrevInstance, const char *lpCmdLine, int64_t nCmdShow) { static atomic_ulong fake_process_signals; @@ -316,10 +347,12 @@ abi int64_t WinMain(int64_t hInstance, int64_t hPrevInstance, __imp_GetSystemInfo(&si); __pagesize = si.dwPageSize; __gransize = si.dwAllocationGranularity; - __pid = __imp_GetCurrentProcessId(); + bool pid_is_inherited; + __pid = WinGetPid(u"_COSMO_PID", &pid_is_inherited); if (!(__sig.process = __sig_map_process(__pid, kNtOpenAlways))) __sig.process = &fake_process_signals; - atomic_store_explicit(__sig.process, 0, memory_order_release); + if (!pid_is_inherited) + atomic_store_explicit(__sig.process, 0, memory_order_release); cmdline = __imp_GetCommandLineW(); #if SYSDEBUG // sloppy flag-only check for early initialization diff --git a/test/posix/pending_signal_execve_test.c b/test/posix/pending_signal_execve_test.c new file mode 100644 index 000000000..326f3b841 --- /dev/null +++ b/test/posix/pending_signal_execve_test.c @@ -0,0 +1,59 @@ +// Copyright 2024 Justine Alexandra Roberts Tunney +// +// Permission to use, copy, modify, and/or distribute this software for +// any purpose with or without fee is hereby granted, provided that the +// above copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +// PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include + +sig_atomic_t gotsig; + +void onsig(int sig) { + gotsig = sig; +} + +int main(int argc, char* argv[]) { + sigset_t ss; + sigfillset(&ss); + sigprocmask(SIG_BLOCK, &ss, 0); + if (argc >= 2 && !strcmp(argv[1], "childe")) { + signal(SIGUSR1, onsig); + sigemptyset(&ss); + sigsuspend(&ss); + if (gotsig != SIGUSR1) + return 2; + } else { + int child; + if ((child = fork()) == -1) + return 2; + if (!child) { + execlp(argv[0], argv[0], "childe", NULL); + _Exit(127); + } + if (IsNetbsd()) { + // NetBSD has a bug where pending signals don't inherit across + // execve, even though POSIX.1 literally says you must do this + sleep(1); + } + if (kill(child, SIGUSR1)) + return 3; + int ws; + if (wait(&ws) != child) + return 4; + if (ws) + return 5; + } +}