cmd/sketch: add a process reaper

When we're PID 1 in a container, reaping zombies
is our responsibility, which we were shirking.

No longer! Now we 🥷 all the 🧟 into ☠️, and not 🐢 either.
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 210db71..6013cba 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -615,6 +615,14 @@
 		os.Setenv("SKETCH_PUB_KEY", pubKey)
 	}
 
+	if inInsideSketch {
+		// Start a process reaper, because we are PID 1.
+		err := startReaper(context.WithoutCancel(ctx))
+		if err != nil {
+			slog.WarnContext(ctx, "failed to start process reaper", "error", err)
+		}
+	}
+
 	wd, err := os.Getwd()
 	if err != nil {
 		return err
diff --git a/cmd/sketch/reaper_linux.go b/cmd/sketch/reaper_linux.go
new file mode 100644
index 0000000..602deb6
--- /dev/null
+++ b/cmd/sketch/reaper_linux.go
@@ -0,0 +1,45 @@
+//go:build linux
+
+package main
+
+import (
+	"context"
+	"log/slog"
+	"os"
+	"os/signal"
+
+	"golang.org/x/sys/unix"
+)
+
+func startReaper(ctx context.Context) error {
+	if err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0); err != nil {
+		return err
+	}
+	go reapZombies(ctx)
+	return nil
+}
+
+// reapZombies runs until ctx is cancelled.
+func reapZombies(ctx context.Context) {
+	sig := make(chan os.Signal, 16)
+	signal.Notify(sig, unix.SIGCHLD)
+
+	for range sig {
+	Reap:
+		for {
+			var status unix.WaitStatus
+			pid, err := unix.Wait4(-1, &status, unix.WNOHANG, nil)
+			switch {
+			case pid > 0:
+				slog.DebugContext(ctx, "reaper ran", "pid", pid, "exit", status.ExitStatus())
+			case err == unix.EINTR:
+				// interrupted: fall through to retry
+			case err == unix.ECHILD || pid == 0:
+				break Reap // no more ready children; wait for next SIGCHLD
+			default:
+				slog.WarnContext(ctx, "wait4 error", "error", err)
+				break Reap
+			}
+		}
+	}
+}
diff --git a/cmd/sketch/reaper_other.go b/cmd/sketch/reaper_other.go
new file mode 100644
index 0000000..6b03d75
--- /dev/null
+++ b/cmd/sketch/reaper_other.go
@@ -0,0 +1,9 @@
+//go:build !linux
+
+package main
+
+import "context"
+
+func startReaper(_ context.Context) error {
+	return nil
+}