Spoof PID across execve() on Windows

It's now possible with cosmo and redbean, to deliver a signal to a child
process after it has called execve(). However the executed program needs
to be compiled using cosmocc. The cosmo runtime WinMain() implementation
now intercepts a _COSMO_PID environment variable that's set by execve().
It ensures the child process will use the same C:\ProgramData\cosmo\sigs
file, which is where kill() will place the delivered signal. We are able
to do this on Windows even better than NetBSD, which has a bug with this

Fixes #1334
This commit is contained in:
Justine Tunney 2024-12-14 12:23:02 -08:00
parent 9cc1bd04b2
commit 26c051c297
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
8 changed files with 187 additions and 21 deletions

View file

@ -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;

View file

@ -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"

View file

@ -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__ */

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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 <cosmo.h>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
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;
}
}