diff --git a/libc/calls/sig.c b/libc/calls/sig.c
index 9c0c6a8daef..e7919cc217a 100644
--- a/libc/calls/sig.c
+++ b/libc/calls/sig.c
@@ -97,6 +97,32 @@ textwindows void __sig_delete(int sig) {
   ALLOW_SIGNALS;
 }
 
+static textwindows int __sig_getter(struct CosmoTib *tib, atomic_ulong *sigs) {
+  int sig;
+  sigset_t bit, pending, masked, deliverable;
+  for (;;) {
+    pending = atomic_load_explicit(sigs, memory_order_acquire);
+    masked = atomic_load_explicit(&tib->tib_sigmask, memory_order_acquire);
+    if ((deliverable = pending & ~masked)) {
+      sig = _bsf(deliverable) + 1;
+      bit = 1ull << (sig - 1);
+      if (atomic_fetch_and_explicit(sigs, ~bit, memory_order_acq_rel) & bit) {
+        return sig;
+      }
+    } else {
+      return 0;
+    }
+  }
+}
+
+static textwindows int __sig_get(struct CosmoTib *tib) {
+  int sig;
+  if (!(sig = __sig_getter(tib, &tib->tib_sigpending))) {
+    sig = __sig_getter(tib, &__sig.pending);
+  }
+  return sig;
+}
+
 static textwindows bool __sig_should_use_altstack(unsigned flags,
                                                   struct CosmoTib *tib) {
   if (!(flags & SA_ONSTACK)) {
@@ -146,34 +172,49 @@ static textwindows sigaction_f __sig_handler(unsigned rva) {
 }
 
 textwindows int __sig_raise(int sig, int sic) {
-  unsigned rva, flags;
+
+  // create machine context object
   struct PosixThread *pt = _pthread_self();
   ucontext_t ctx = {.uc_sigmask = pt->tib->tib_sigmask};
-  if (!__sig_start(pt, sig, &rva, &flags)) return 0;
-  if (flags & SA_RESETHAND) {
-    STRACE("resetting %G handler", sig);
-    __sighandrvas[sig] = (int32_t)(intptr_t)SIG_DFL;
-  }
-  siginfo_t si = {.si_signo = sig, .si_code = sic};
   struct NtContext nc;
   nc.ContextFlags = kNtContextFull;
   GetThreadContext(GetCurrentThread(), &nc);
   _ntcontext2linux(&ctx, &nc);
-  pt->tib->tib_sigmask |= __sighandmask[sig];
-  if (!(flags & SA_NODEFER)) {
-    pt->tib->tib_sigmask |= 1ull << (sig - 1);
-  }
-  NTTRACE("entering raise(%G) signal handler %t with mask %s → %s", sig,
-          __sig_handler(rva), DescribeSigset(0, &ctx.uc_sigmask),
-          DescribeSigset(0, (sigset_t *)&pt->tib->tib_sigmask));
-  __sig_handler(rva)(sig, &si, &ctx);
-  NTTRACE("leaving raise(%G) signal handler %t with mask %s → %s", sig,
-          __sig_handler(rva),
-          DescribeSigset(0, (sigset_t *)&pt->tib->tib_sigmask),
-          DescribeSigset(0, &ctx.uc_sigmask));
-  atomic_store_explicit(&pt->tib->tib_sigmask, ctx.uc_sigmask,
-                        memory_order_release);
-  return (flags & SA_RESTART) ? 2 : 1;
+
+  // process signal(s)
+  int handler_was_called = 0;
+  do {
+    // start the signal
+    unsigned rva, flags;
+    if (__sig_start(pt, sig, &rva, &flags)) {
+      if (flags & SA_RESETHAND) {
+        STRACE("resetting %G handler", sig);
+        __sighandrvas[sig] = (int32_t)(intptr_t)SIG_DFL;
+      }
+
+      // update the signal mask in preparation for signal handller
+      sigset_t blocksigs = __sighandmask[sig];
+      if (!(flags & SA_NODEFER)) blocksigs |= 1ull << (sig - 1);
+      ctx.uc_sigmask = atomic_fetch_or_explicit(
+          &pt->tib->tib_sigmask, blocksigs, memory_order_acq_rel);
+
+      // call the user's signal handler
+      char ssbuf[2][128];
+      siginfo_t si = {.si_signo = sig, .si_code = sic};
+      STRACE("__sig_raise(%G, %t) mask %s → %s", sig, __sig_handler(rva),
+             (DescribeSigset)(ssbuf[0], 0, &ctx.uc_sigmask),
+             (DescribeSigset)(ssbuf[1], 0, (sigset_t *)&pt->tib->tib_sigmask));
+      __sig_handler(rva)(sig, &si, &ctx);
+      (void)ssbuf;
+
+      // restore the original signal mask
+      atomic_store_explicit(&pt->tib->tib_sigmask, ctx.uc_sigmask,
+                            memory_order_release);
+      handler_was_called |= (flags & SA_RESTART) ? 2 : 1;
+    }
+    sic = SI_KERNEL;
+  } while ((sig = __sig_get(pt->tib)));
+  return handler_was_called;
 }
 
 // cancels blocking operations being performed by signaled thread
@@ -220,17 +261,48 @@ textwindows void __sig_cancel(struct PosixThread *pt, int sig, unsigned flags) {
   WakeByAddressSingle(blocker);
 }
 
+// the user's signal handler callback is composed with this trampoline
 static textwindows wontreturn void __sig_tramp(struct SignalFrame *sf) {
-  atomic_fetch_add_explicit(&__sig.count, 1, memory_order_relaxed);
   int sig = sf->si.si_signo;
-  sigset_t blocksigs = __sighandmask[sig];
-  if (!(sf->flags & SA_NODEFER)) blocksigs |= 1ull << (sig - 1);
-  sf->ctx.uc_sigmask = atomic_fetch_or_explicit(
-      &__get_tls()->tib_sigmask, blocksigs, memory_order_acq_rel);
-  __sig_handler(sf->rva)(sig, &sf->si, &sf->ctx);
-  atomic_store_explicit(&__get_tls()->tib_sigmask, sf->ctx.uc_sigmask,
-                        memory_order_release);
-  __sig_restore(&sf->ctx);
+  struct CosmoTib *tib = __get_tls();
+  struct PosixThread *pt = (struct PosixThread *)tib->tib_pthread;
+  for (;;) {
+    atomic_fetch_add_explicit(&__sig.count, 1, memory_order_relaxed);
+
+    // update the signal mask in preparation for signal handller
+    sigset_t blocksigs = __sighandmask[sig];
+    if (!(sf->flags & SA_NODEFER)) blocksigs |= 1ull << (sig - 1);
+    sf->ctx.uc_sigmask = atomic_fetch_or_explicit(&tib->tib_sigmask, blocksigs,
+                                                  memory_order_acq_rel);
+
+    // call the user's signal handler
+    char ssbuf[2][128];
+    STRACE("__sig_tramp(%G, %t) mask %s → %s", sig, __sig_handler(sf->rva),
+           (DescribeSigset)(ssbuf[0], 0, &sf->ctx.uc_sigmask),
+           (DescribeSigset)(ssbuf[1], 0, (sigset_t *)&tib->tib_sigmask));
+    __sig_handler(sf->rva)(sig, &sf->si, &sf->ctx);
+    (void)ssbuf;
+
+    // restore the signal mask that was used by the interrupted code
+    // this may have been modified by the signal handler in the callback
+    atomic_store_explicit(&tib->tib_sigmask, sf->ctx.uc_sigmask,
+                          memory_order_release);
+
+    // jump back into original code if there aren't any pending signals
+    do {
+      if (!(sig = __sig_get(tib))) {
+        __sig_restore(&sf->ctx);
+      }
+    } while (!__sig_start(pt, sig, &sf->rva, &sf->flags));
+
+    // tail recurse into another signal handler
+    sf->si.si_signo = sig;
+    sf->si.si_code = SI_KERNEL;
+    if (sf->flags & SA_RESETHAND) {
+      STRACE("resetting %G handler", sig);
+      __sighandrvas[sig] = (int32_t)(intptr_t)SIG_DFL;
+    }
+  }
 }
 
 static int __sig_killer(struct PosixThread *pt, int sig, int sic) {
@@ -266,7 +338,7 @@ static int __sig_killer(struct PosixThread *pt, int sig, int sic) {
   if ((pt->tib->tib_sigmask & (1ull << (sig - 1))) ||
       !((uintptr_t)__executable_start <= nc.Rip &&
         nc.Rip < (uintptr_t)__privileged_start)) {
-    STRACE("enqueing %G on %d", sig, _pthread_tid(pt));
+    STRACE("enqueing %G on %d rip %p", sig, _pthread_tid(pt), nc.Rip);
     pt->tib->tib_sigpending |= 1ull << (sig - 1);
     ResumeThread(th);
     __sig_cancel(pt, sig, flags);
@@ -345,11 +417,11 @@ textwindows void __sig_generate(int sig, int sic) {
   if (mark) {
     STRACE("generating %G by killing %d", sig, _pthread_tid(mark));
     __sig_killer(mark, sig, sic);
+    _pthread_unref(mark);
   } else {
     STRACE("all threads block %G so adding to pending signals of process", sig);
     __sig.pending |= 1ull << (sig - 1);
   }
-  _pthread_unref(mark);
   ALLOW_SIGNALS;
 }
 
@@ -539,34 +611,17 @@ __msabi textwindows dontinstrument bool32 __sig_console(uint32_t dwCtrlType) {
   return true;
 }
 
-static textwindows int __sig_checker(atomic_ulong *sigs, struct CosmoTib *tib) {
-  int sig, handler_was_called = 0;
-  sigset_t bit, pending, masked, deliverable;
-  pending = atomic_load_explicit(sigs, memory_order_acquire);
-  masked = atomic_load_explicit(&tib->tib_sigmask, memory_order_acquire);
-  if ((deliverable = pending & ~masked)) {
-    do {
-      sig = _bsf(deliverable) + 1;
-      bit = 1ull << (sig - 1);
-      if (atomic_fetch_and_explicit(sigs, ~bit, memory_order_acq_rel) & bit) {
-        STRACE("found pending %G we can raise now", sig);
-        handler_was_called |= __sig_raise(sig, SI_KERNEL);
-      }
-    } while ((deliverable &= ~bit));
-  }
-  return handler_was_called;
-}
-
 // returns 0 if no signal handlers were called, otherwise a bitmask
 // consisting of `1` which means a signal handler was invoked which
 // didn't have the SA_RESTART flag, and `2`, which means SA_RESTART
 // handlers were called (or `3` if both were the case).
 textwindows int __sig_check(void) {
-  int handler_was_called = false;
-  struct CosmoTib *tib = __get_tls();
-  handler_was_called |= __sig_checker(&tib->tib_sigpending, tib);
-  handler_was_called |= __sig_checker(&__sig.pending, tib);
-  return handler_was_called;
+  int sig;
+  if ((sig = __sig_get(__get_tls()))) {
+    return __sig_raise(sig, SI_KERNEL);
+  } else {
+    return 0;
+  }
 }
 
 textstartup void __sig_init(void) {
diff --git a/libc/intrin/sig.c b/libc/intrin/sig.c
index c33121a49bf..03c037c5635 100644
--- a/libc/intrin/sig.c
+++ b/libc/intrin/sig.c
@@ -29,7 +29,7 @@ struct Signals __sig;
 sigset_t __sig_block(void) {
   if (IsWindows()) {
     return atomic_exchange_explicit(&__get_tls()->tib_sigmask, -1,
-                                    memory_order_acq_rel);
+                                    memory_order_acquire);
   } else {
     sigset_t res, neu = -1;
     sys_sigprocmask(SIG_SETMASK, &neu, &res);
@@ -55,7 +55,7 @@ textwindows int __sig_enqueue(int sig) {
 
 textwindows sigset_t __sig_beginwait(sigset_t waitmask) {
   return atomic_exchange_explicit(&__get_tls()->tib_sigmask, waitmask,
-                                  memory_order_acq_rel);
+                                  memory_order_acquire);
 }
 
 textwindows void __sig_finishwait(sigset_t m) {
diff --git a/libc/nt/master.sh b/libc/nt/master.sh
index 45677c1d224..471f246959d 100755
--- a/libc/nt/master.sh
+++ b/libc/nt/master.sh
@@ -598,6 +598,10 @@ imp	'PdhOpenQuery'						PdhOpenQueryW						pdh		3	# Creates a new query that is
 # PSAPI.DLL
 #
 #	Name							Actual							DLL		Arity
+imp	'EnumProcessModules'					EnumProcessModules					psapi		4
+imp	'EnumProcessModulesEx'					EnumProcessModulesEx					psapi		5
+imp	'EnumProcesses'						EnumProcesses						psapi		3
+imp	'GetModuleBaseName'					GetModuleBaseNameW					psapi		4
 imp	'GetProcessImageFileName'				GetProcessImageFileNameW				psapi		3
 imp	'GetProcessMemoryInfo'					GetProcessMemoryInfo					psapi		3
 
diff --git a/libc/nt/process.h b/libc/nt/process.h
index 8be6870292b..7f212753519 100644
--- a/libc/nt/process.h
+++ b/libc/nt/process.h
@@ -78,6 +78,17 @@ int64_t CreateToolhelp32Snapshot(uint32_t dwFlags, uint32_t th32ProcessID);
 bool32 Process32First(int64_t hSnapshot, struct NtProcessEntry32 *in_out_lppe);
 bool32 Process32Next(int64_t hSnapshot, struct NtProcessEntry32 *out_lppe);
 
+bool32 EnumProcesses(uint32_t *out_lpidProcess, uint32_t cb,
+                     uint32_t *out_lpcbNeeded) paramsnonnull();
+bool32 EnumProcessModules(int64_t hProcess, int64_t *out_lphModule, uint32_t cb,
+                          uint32_t *out_lpcbNeeded) paramsnonnull();
+bool32 EnumProcessModulesEx(int64_t hProcess, int64_t *out_lphModule,
+                            uint32_t cb, uint32_t *out_lpcbNeeded,
+                            uint32_t dwFilterFlag) paramsnonnull();
+uint32_t GetModuleBaseName(int64_t hProcess, int64_t opt_hModule,
+                           char16_t *out_lpBaseName, uint32_t nSize)
+    paramsnonnull();
+
 #if ShouldUseMsabiAttribute()
 #include "libc/nt/thunk/process.inc"
 #endif /* ShouldUseMsabiAttribute() */
diff --git a/libc/nt/psapi/EnumProcessModules.S b/libc/nt/psapi/EnumProcessModules.S
new file mode 100644
index 00000000000..1ef1a774455
--- /dev/null
+++ b/libc/nt/psapi/EnumProcessModules.S
@@ -0,0 +1,18 @@
+#include "libc/nt/codegen.h"
+.imp	psapi,__imp_EnumProcessModules,EnumProcessModules
+
+	.text.windows
+        .ftrace1
+EnumProcessModules:
+        .ftrace2
+#ifdef __x86_64__
+	push	%rbp
+	mov	%rsp,%rbp
+	mov	__imp_EnumProcessModules(%rip),%rax
+	jmp	__sysv2nt
+#elif defined(__aarch64__)
+	mov	x0,#0
+	ret
+#endif
+	.endfn	EnumProcessModules,globl
+	.previous
diff --git a/libc/nt/psapi/EnumProcessModulesEx.S b/libc/nt/psapi/EnumProcessModulesEx.S
new file mode 100644
index 00000000000..b9526f3cd33
--- /dev/null
+++ b/libc/nt/psapi/EnumProcessModulesEx.S
@@ -0,0 +1,18 @@
+#include "libc/nt/codegen.h"
+.imp	psapi,__imp_EnumProcessModulesEx,EnumProcessModulesEx
+
+	.text.windows
+        .ftrace1
+EnumProcessModulesEx:
+        .ftrace2
+#ifdef __x86_64__
+	push	%rbp
+	mov	%rsp,%rbp
+	mov	__imp_EnumProcessModulesEx(%rip),%rax
+	jmp	__sysv2nt6
+#elif defined(__aarch64__)
+	mov	x0,#0
+	ret
+#endif
+	.endfn	EnumProcessModulesEx,globl
+	.previous
diff --git a/libc/nt/psapi/EnumProcesses.S b/libc/nt/psapi/EnumProcesses.S
new file mode 100644
index 00000000000..46682c40c36
--- /dev/null
+++ b/libc/nt/psapi/EnumProcesses.S
@@ -0,0 +1,18 @@
+#include "libc/nt/codegen.h"
+.imp	psapi,__imp_EnumProcesses,EnumProcesses
+
+	.text.windows
+        .ftrace1
+EnumProcesses:
+        .ftrace2
+#ifdef __x86_64__
+	push	%rbp
+	mov	%rsp,%rbp
+	mov	__imp_EnumProcesses(%rip),%rax
+	jmp	__sysv2nt
+#elif defined(__aarch64__)
+	mov	x0,#0
+	ret
+#endif
+	.endfn	EnumProcesses,globl
+	.previous
diff --git a/libc/nt/psapi/GetModuleBaseNameW.S b/libc/nt/psapi/GetModuleBaseNameW.S
new file mode 100644
index 00000000000..6210a49ace8
--- /dev/null
+++ b/libc/nt/psapi/GetModuleBaseNameW.S
@@ -0,0 +1,18 @@
+#include "libc/nt/codegen.h"
+.imp	psapi,__imp_GetModuleBaseNameW,GetModuleBaseNameW
+
+	.text.windows
+        .ftrace1
+GetModuleBaseName:
+        .ftrace2
+#ifdef __x86_64__
+	push	%rbp
+	mov	%rsp,%rbp
+	mov	__imp_GetModuleBaseNameW(%rip),%rax
+	jmp	__sysv2nt
+#elif defined(__aarch64__)
+	mov	x0,#0
+	ret
+#endif
+	.endfn	GetModuleBaseName,globl
+	.previous
diff --git a/libc/proc/fork-nt.c b/libc/proc/fork-nt.c
index d0e7a7fa6a0..bdcd0db5bcc 100644
--- a/libc/proc/fork-nt.c
+++ b/libc/proc/fork-nt.c
@@ -395,6 +395,7 @@ textwindows int sys_fork_nt(uint32_t dwCreationFlags) {
     __set_tls(tib);
     __morph_tls();
     __tls_enabled_set(true);
+    // get new main thread handle
     // clear pending signals
     tib->tib_sigpending = 0;
     atomic_store_explicit(&__sig.pending, 0, memory_order_relaxed);
@@ -404,9 +405,9 @@ textwindows int sys_fork_nt(uint32_t dwCreationFlags) {
     if (ftrace_stackdigs) {
       _weaken(__hook)(_weaken(ftrace_hook), _weaken(GetSymbolTable)());
     }
-    // reset console
+    // reset core runtime services
+    __proc_wipe();
     __keystroke_wipe();
-    // reset alarms
     if (_weaken(__itimer_wipe)) {
       _weaken(__itimer_wipe)();
     }
diff --git a/libc/proc/fork.c b/libc/proc/fork.c
index 945c0a47f60..ac69bbd0c38 100644
--- a/libc/proc/fork.c
+++ b/libc/proc/fork.c
@@ -27,21 +27,22 @@
 #include "libc/intrin/dll.h"
 #include "libc/intrin/strace.internal.h"
 #include "libc/intrin/weaken.h"
+#include "libc/nt/files.h"
 #include "libc/nt/process.h"
 #include "libc/nt/runtime.h"
 #include "libc/nt/synchronization.h"
+#include "libc/nt/thread.h"
 #include "libc/proc/proc.internal.h"
 #include "libc/runtime/internal.h"
 #include "libc/runtime/runtime.h"
+#include "libc/runtime/syslib.internal.h"
 #include "libc/sysv/consts/sig.h"
 #include "libc/thread/posixthread.internal.h"
 #include "libc/thread/tls.h"
 
 int _fork(uint32_t dwCreationFlags) {
   struct Dll *e;
-  struct CosmoTib *tib;
   int ax, dx, tid, parent;
-  struct PosixThread *me, *other;
   parent = __pid;
   (void)parent;
   BLOCK_SIGNALS;
@@ -55,33 +56,55 @@ int _fork(uint32_t dwCreationFlags) {
     ax = sys_fork_nt(dwCreationFlags);
   }
   if (!ax) {
+
+    // get new process id
     if (!IsWindows()) {
       dx = sys_getpid().ax;
     } else {
       dx = GetCurrentProcessId();
     }
     __pid = dx;
-    tib = __get_tls();
-    me = (struct PosixThread *)tib->tib_pthread;
-    dll_remove(&_pthread_list, &me->list);
+
+    // turn other threads into zombies
+    // we can't free() them since we're monopolizing all locks
+    // we assume the operating system already reclaimed system handles
+    struct CosmoTib *tib = __get_tls();
+    struct PosixThread *pt = (struct PosixThread *)tib->tib_pthread;
+    dll_remove(&_pthread_list, &pt->list);
     for (e = dll_first(_pthread_list); e; e = dll_next(_pthread_list, e)) {
-      other = POSIXTHREAD_CONTAINER(e);
-      atomic_store_explicit(&other->pt_status, kPosixThreadZombie,
+      atomic_store_explicit(&POSIXTHREAD_CONTAINER(e)->pt_status,
+                            kPosixThreadZombie, memory_order_relaxed);
+      atomic_store_explicit(&POSIXTHREAD_CONTAINER(e)->tib->tib_syshand, 0,
                             memory_order_relaxed);
     }
-    dll_make_first(&_pthread_list, &me->list);
+    dll_make_first(&_pthread_list, &pt->list);
+
+    // get new main thread id
     tid = IsLinux() || IsXnuSilicon() ? dx : sys_gettid();
     atomic_store_explicit(&tib->tib_tid, tid, memory_order_relaxed);
-    atomic_store_explicit(&me->ptid, tid, memory_order_relaxed);
-    atomic_store_explicit(&me->pt_canceled, false, memory_order_relaxed);
+    atomic_store_explicit(&pt->ptid, tid, memory_order_relaxed);
+
+    // get new system thread handle
+    intptr_t syshand = 0;
+    if (IsXnuSilicon()) {
+      syshand = __syslib->__pthread_self();
+    } else if (IsWindows()) {
+      DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
+                      GetCurrentProcess(), &syshand, 0, false,
+                      kNtDuplicateSameAccess);
+    }
+    atomic_store_explicit(&tib->tib_syshand, syshand, memory_order_relaxed);
+
+    // we can't be canceled if the canceler no longer exists
+    atomic_store_explicit(&pt->pt_canceled, false, memory_order_relaxed);
+
+    // run user fork callbacks
     if (__threaded && _weaken(_pthread_onfork_child)) {
       _weaken(_pthread_onfork_child)();
     }
-    if (IsWindows()) {
-      __proc_wipe();
-    }
     STRACE("fork() → 0 (child of %d)", parent);
   } else {
+    // this is the parent process
     if (__threaded && _weaken(_pthread_onfork_parent)) {
       _weaken(_pthread_onfork_parent)();
     }
diff --git a/libc/runtime/enable_tls.c b/libc/runtime/enable_tls.c
index 747ed282feb..ccc96fb990e 100644
--- a/libc/runtime/enable_tls.c
+++ b/libc/runtime/enable_tls.c
@@ -27,6 +27,7 @@
 #include "libc/intrin/weaken.h"
 #include "libc/macros.internal.h"
 #include "libc/nt/files.h"
+#include "libc/nt/process.h"
 #include "libc/nt/runtime.h"
 #include "libc/nt/synchronization.h"
 #include "libc/nt/thread.h"
@@ -191,10 +192,11 @@ textstartup void __enable_tls(void) {
   tib->tib_locale = (intptr_t)&__c_dot_utf8_locale;
   tib->tib_pthread = (pthread_t)&_pthread_static;
   if (IsWindows()) {
-    intptr_t threadhand, pseudo = GetCurrentThread();
-    DuplicateHandle(GetCurrentProcess(), pseudo, GetCurrentProcess(),
-                    &threadhand, 0, true, kNtDuplicateSameAccess);
-    atomic_store_explicit(&tib->tib_syshand, threadhand, memory_order_relaxed);
+    intptr_t hThread;
+    DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
+                    GetCurrentProcess(), &hThread, 0, false,
+                    kNtDuplicateSameAccess);
+    atomic_store_explicit(&tib->tib_syshand, hThread, memory_order_relaxed);
   } else if (IsXnuSilicon()) {
     tib->tib_syshand = __syslib->__pthread_self();
   }
diff --git a/test/libc/proc/handkill_test.c b/test/libc/proc/handkill_test.c
new file mode 100644
index 00000000000..8bdc14083e3
--- /dev/null
+++ b/test/libc/proc/handkill_test.c
@@ -0,0 +1,151 @@
+/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
+│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :vi│
+╞══════════════════════════════════════════════════════════════════════════════╡
+│ Copyright 2023 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 "libc/atomic.h"
+#include "libc/calls/calls.h"
+#include "libc/calls/struct/sigaction.h"
+#include "libc/calls/struct/sigset.h"
+#include "libc/dce.h"
+#include "libc/nt/process.h"
+#include "libc/nt/thread.h"
+#include "libc/runtime/clktck.h"
+#include "libc/runtime/runtime.h"
+#include "libc/str/str.h"
+#include "libc/sysv/consts/sig.h"
+#include "libc/testlib/subprocess.h"
+#include "libc/testlib/testlib.h"
+#include "libc/thread/posixthread.internal.h"
+#include "libc/thread/thread.h"
+
+// test we can:
+// 1. raise the same signal inside the signal handler
+// 2. that the signal will trigger immediately after handling
+
+struct SharedMemory {
+  pthread_t target;
+  atomic_bool ready;
+  atomic_bool got_signal;
+  atomic_bool handler_returned;
+} * shm;
+
+void OnSig(int sig) {
+  signal(SIGUSR1, SIG_DFL);
+  raise(SIGUSR1);
+  shm->got_signal = true;
+}
+
+void SetUpOnce(void) {
+  signal(SIGUSR1, OnSig);
+  shm = _mapshared(sizeof(*shm));
+}
+
+void TearDownOnce(void) {
+  munmap(shm, sizeof(*shm));
+}
+
+void SetUp(void) {
+  bzero(shm, sizeof(*shm));
+}
+
+void *Killer(void *arg) {
+  ASSERT_EQ(0, pthread_kill(shm->target, SIGUSR1));
+  return 0;
+}
+
+void *Killed(void *arg) {
+  shm->ready = true;
+  while (!shm->got_signal) donothing;
+  shm->handler_returned = true;
+  return 0;
+}
+
+TEST(handkill, raise) {
+  SPAWN(fork);
+  raise(SIGUSR1);
+  shm->handler_returned = true;
+  TERMS(SIGUSR1);
+  EXPECT_TRUE(shm->got_signal);
+  EXPECT_FALSE(shm->handler_returned);
+}
+
+TEST(handkill, main2thread_async) {
+  SPAWN(fork);
+  pthread_t th;
+  shm->target = pthread_self();
+  pthread_create(&th, 0, Killed, 0);
+  while (!shm->ready) donothing;
+  ASSERT_EQ(0, pthread_kill(th, SIGUSR1));
+  ASSERT_EQ(0, pthread_join(th, 0));
+  TERMS(SIGUSR1);
+  EXPECT_TRUE(shm->got_signal);
+  EXPECT_FALSE(shm->handler_returned);
+}
+
+TEST(handkill, thread2main_async) {
+  SPAWN(fork);
+  pthread_t th;
+  shm->target = pthread_self();
+  pthread_create(&th, 0, Killer, 0);
+  while (!shm->got_signal) donothing;
+  shm->handler_returned = true;
+  pthread_join(th, 0);
+  TERMS(SIGUSR1);
+  EXPECT_TRUE(shm->got_signal);
+  EXPECT_FALSE(shm->handler_returned);
+}
+
+TEST(handkill, thread2thread_async) {
+  SPAWN(fork);
+  pthread_t th;
+  pthread_create(&shm->target, 0, Killed, 0);
+  pthread_create(&th, 0, Killer, 0);
+  pthread_join(shm->target, 0);
+  pthread_join(th, 0);
+  TERMS(SIGUSR1);
+  EXPECT_TRUE(shm->got_signal);
+  EXPECT_FALSE(shm->handler_returned);
+}
+
+TEST(handkill, process_async) {
+  if (IsWindows()) return;
+  SPAWN(fork);
+  shm->ready = true;
+  while (!shm->got_signal) donothing;
+  shm->handler_returned = true;
+  PARENT();
+  while (!shm->ready) donothing;
+  ASSERT_SYS(0, 0, kill(child, SIGUSR1));
+  WAIT(term, SIGUSR1);
+  EXPECT_TRUE(shm->got_signal);
+  EXPECT_FALSE(shm->handler_returned);
+}
+
+TEST(handkill, process_pause) {
+  if (IsWindows()) return;
+  SPAWN(fork);
+  shm->ready = true;
+  pause();
+  shm->handler_returned = true;
+  PARENT();
+  while (!shm->ready) donothing;
+  usleep(1e6 / CLK_TCK * 2);
+  ASSERT_SYS(0, 0, kill(child, SIGUSR1));
+  WAIT(term, SIGUSR1);
+  EXPECT_TRUE(shm->got_signal);
+  EXPECT_FALSE(shm->handler_returned);
+}
diff --git a/test/libc/thread/pthread_kill_test.c b/test/libc/thread/pthread_kill_test.c
index a74810606c5..e6040359ba1 100644
--- a/test/libc/thread/pthread_kill_test.c
+++ b/test/libc/thread/pthread_kill_test.c
@@ -22,6 +22,7 @@
 #include "libc/dce.h"
 #include "libc/errno.h"
 #include "libc/intrin/kprintf.h"
+#include "libc/runtime/clktck.h"
 #include "libc/runtime/runtime.h"
 #include "libc/sock/sock.h"
 #include "libc/sock/struct/sockaddr.h"
diff --git a/tool/build/build.mk b/tool/build/build.mk
index ada3f0b9887..7d1b0d74b5c 100644
--- a/tool/build/build.mk
+++ b/tool/build/build.mk
@@ -34,6 +34,7 @@ TOOL_BUILD_DIRECTDEPS =					\
 	LIBC_MEM					\
 	LIBC_NEXGEN32E					\
 	LIBC_NT_KERNEL32				\
+	LIBC_NT_PSAPI					\
 	LIBC_NT_USER32					\
 	LIBC_NT_WS2_32					\
 	LIBC_PROC					\
diff --git a/tool/build/killall.c b/tool/build/killall.c
new file mode 100644
index 00000000000..1d5f9ea8aee
--- /dev/null
+++ b/tool/build/killall.c
@@ -0,0 +1,229 @@
+/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
+│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :vi│
+╞══════════════════════════════════════════════════════════════════════════════╡
+│ Copyright 2023 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 "libc/dce.h"
+#include "libc/limits.h"
+#include "libc/macros.internal.h"
+#include "libc/mem/mem.h"
+#include "libc/nt/enum/formatmessageflags.h"
+#include "libc/nt/enum/lang.h"
+#include "libc/nt/enum/processaccess.h"
+#include "libc/nt/process.h"
+#include "libc/nt/runtime.h"
+#include "libc/runtime/runtime.h"
+#include "libc/stdio/internal.h"
+#include "libc/stdio/stdio.h"
+#include "libc/str/str.h"
+#include "libc/sysv/consts/sig.h"
+#include "libc/x/x.h"
+#include "third_party/getopt/getopt.internal.h"
+
+/**
+ * @fileoverview Mass Process Killer for Windows.
+ *
+ * Bad things sometimes happen during development, such as fork bombs.
+ * Under such circumstances, the Windows operating system itself remains
+ * remarkably stable (much more so than Linux would in these cases) but
+ * the tools on Windows for managing processes do not scale gracefully;
+ * GUIs like the Task Manager and Process Explorer become unresponsive
+ * when the system has a nontrivial number of processes, leaving no way
+ * to kill the processes. This tool is designed to make it easy to kill
+ * processes, particularly in large numbers, via a simple CLI interface.
+ */
+
+static const char *prog;
+static char16_t **filters;
+static uint32_t pids[10000];
+
+static wontreturn void PrintUsage(int rc, FILE *f) {
+  fprintf(f,
+          "Usage: %s [-nshv] NAME...\n"
+          "\tNAME\tcase-insensitive process name substring filter\n"
+          "\t-n\tdry run (print matching processes but do not kill)\n"
+          "\t-v\tverbose mode\n"
+          "\t-s\tsilent mode\n"
+          "\t-h\tshow help\n",
+          prog);
+  exit(rc);
+}
+
+static wontreturn void OutOfMemory(void) {
+  fprintf(stderr, "%s: out of memory\n", prog);
+  exit(1);
+}
+
+static void *Calloc(size_t n, size_t z) {
+  void *p;
+  if (!(p = calloc(n, z))) OutOfMemory();
+  return p;
+}
+
+static void ConvertStringToLowercase16(char16_t *s) {
+  while (*s) {
+    *s = towlower(*s);
+    ++s;
+  }
+}
+
+static bool ShouldKillProcess(char16_t *name) {
+  if (!*filters) {
+    return true;
+  }
+  for (char16_t **f = filters; *f; ++f) {
+    if (strstr16(name, *f)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+static int64_t MyOpenProcess(uint32_t pid) {
+  return OpenProcess(
+      kNtProcessTerminate | kNtProcessQueryInformation | kNtProcessVmRead,
+      false, pid);
+}
+
+static bool GetProcessName(int64_t hand, char16_t name[hasatleast PATH_MAX]) {
+  int64_t hMod;
+  uint32_t cbNeeded;
+  *name = 0;
+  if (EnumProcessModules(hand, &hMod, sizeof(hMod), &cbNeeded)) {
+    GetModuleBaseName(hand, hMod, name, PATH_MAX);
+  }
+  return !!name[0];
+}
+
+static char16_t *DescribeError(int err) {
+  static char16_t msg[256];
+  FormatMessage(kNtFormatMessageFromSystem | kNtFormatMessageIgnoreInserts, 0,
+                err, MAKELANGID(kNtLangNeutral, kNtSublangDefault), msg,
+                ARRAYLEN(msg), 0);
+  return chomp16(msg);
+}
+
+int main(int argc, char *argv[]) {
+  int i, j;
+
+  // get program name
+  prog = argv[0] ? argv[0] : "killall";
+
+  // sanity check environment
+  if (!IsWindows()) {
+    fprintf(stderr, "%s: this program is intended for windows\n", prog);
+    exit(1);
+  }
+
+  // try to minimize terminal writes slowing us down
+  setvbuf(stdin, NULL, _IONBF, 0);
+
+  // get flags
+  int opt;
+  bool dryrun = false;
+  bool silent = false;
+  bool verbose = false;
+  while ((opt = getopt(argc, argv, "nhsv")) != -1) {
+    switch (opt) {
+      case 'n':
+        dryrun = true;
+        break;
+      case 's':
+        silent = true;
+        break;
+      case 'v':
+        verbose = true;
+        break;
+      case 'h':
+        PrintUsage(0, stdout);
+      default:
+        PrintUsage(1, stderr);
+    }
+  }
+
+  // get names of things to kill
+  filters = Calloc(argc, sizeof(char16_t *));
+  for (j = 0, i = optind; i < argc; ++i) {
+    char16_t *filter;
+    if ((filter = utf8to16(argv[i], -1, 0)) && *filter) {
+      ConvertStringToLowercase16(filter);
+      filters[j++] = filter;
+    }
+  }
+  if (!j && !dryrun) {
+    fprintf(stderr, "%s: missing operand\n", prog);
+    exit(1);
+  }
+
+  // outer loop
+  int count = 0;
+  int subcount;
+  do {
+
+    // get pids of all processes on system
+    uint32_t n;
+    if (!EnumProcesses(pids, sizeof(pids), &n)) {
+      fprintf(stderr, "%s: EnumProcesses() failed: %s\n", prog,
+              DescribeError(GetLastError()));
+      exit(1);
+    }
+    n /= sizeof(uint32_t);
+
+    // kill matching processes
+    int64_t hProcess;
+    char16_t name[PATH_MAX];
+    for (subcount = i = 0; i < n; i++) {
+      if (!pids[i]) continue;
+      if ((hProcess = MyOpenProcess(pids[i]))) {
+        if (GetProcessName(hProcess, name)) {
+          ConvertStringToLowercase16(name);
+          if (ShouldKillProcess(name)) {
+            if (dryrun) {
+              if (!silent) {
+                printf("%5u %hs\n", pids[i], name);
+              }
+              ++subcount;
+            } else if (TerminateProcess(hProcess, SIGKILL)) {
+              if (!silent) {
+                printf("%5u %hs killed\n", pids[i], name);
+              }
+              ++subcount;
+            } else {
+              printf("%5u %hs %hs\n", pids[i], name,
+                     DescribeError(GetLastError()));
+            }
+          }
+        } else if (verbose) {
+          fprintf(stderr, "%s: GetProcessName(%u) failed: %hs\n", prog, pids[i],
+                  DescribeError(GetLastError()));
+        }
+        CloseHandle(hProcess);
+      } else if (verbose) {
+        fprintf(stderr, "%s: OpenProcess(%u) failed: %hs\n", prog, pids[i],
+                DescribeError(GetLastError()));
+      }
+    }
+
+    // we don't stop until we've confirmed they're all gone
+    count += subcount;
+  } while (!dryrun && subcount);
+
+  if (!silent && !count) {
+    fprintf(stderr, "%s: no processes found\n", prog);
+  }
+
+  return 0;
+}