Skip to content

Commit

Permalink
Spoof PID across execve() on Windows
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jart committed Dec 14, 2024
1 parent 9cc1bd0 commit 26c051c
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 21 deletions.
5 changes: 5 additions & 0 deletions libc/intrin/sig.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion libc/intrin/terminatethisprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 34 additions & 12 deletions libc/proc/execve-nt.greg.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -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__ */
49 changes: 45 additions & 4 deletions libc/proc/execve.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions libc/proc/kill-nt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions libc/proc/kill.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 35 additions & 2 deletions libc/runtime/winmain.greg.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
59 changes: 59 additions & 0 deletions test/posix/pending_signal_execve_test.c
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 26c051c

Please sign in to comment.