dockerimg: add seccomp filter to protect PID 1 from kill syscalls

Add automatic seccomp profile creation and injection to prevent sketch
processes from killing themselves via pkill -f or similar commands that
target PID 1 within Docker containers.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s74e3bcc6e17ec376k
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index ac07fa8..e708745 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -535,6 +535,14 @@
 	// colima does this by default, but Linux docker seems to need this set explicitly
 	cmdArgs = append(cmdArgs, "--add-host", "host.docker.internal:host-gateway")
 
+	// Add seccomp profile to prevent killing PID 1 (the sketch process itself)
+	// Write the seccomp profile to cache directory if it doesn't exist
+	seccompPath, err := ensureSeccompProfile(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to create seccomp profile: %w", err)
+	}
+	cmdArgs = append(cmdArgs, "--security-opt", "seccomp="+seccompPath)
+
 	// Add volume mounts if specified
 	for _, mount := range config.Mounts {
 		if mount != "" {
@@ -1188,3 +1196,47 @@
 	}
 	return strings.TrimSpace(string(out)), nil
 }
+
+const seccompProfile = `{
+  "defaultAction": "SCMP_ACT_ALLOW",
+  "syscalls": [
+    {
+      "names": ["kill", "tkill", "tgkill", "pidfd_send_signal"],
+      "action": "SCMP_ACT_ERRNO",
+      "args": [
+        {
+          "index": 0,
+          "value": 1,
+          "op": "SCMP_CMP_EQ"
+        }
+      ]
+    }
+  ]
+}`
+
+// ensureSeccompProfile creates the seccomp profile file in the sketch cache directory if it doesn't exist.
+func ensureSeccompProfile(ctx context.Context) (seccompPath string, err error) {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		return "", fmt.Errorf("failed to get home directory: %w", err)
+	}
+	cacheDir := filepath.Join(homeDir, ".cache", "sketch")
+	if err := os.MkdirAll(cacheDir, 0o755); err != nil {
+		return "", fmt.Errorf("failed to create cache directory: %w", err)
+	}
+	seccompPath = filepath.Join(cacheDir, "seccomp-no-kill-1.json")
+
+	curBytes, err := os.ReadFile(seccompPath)
+	if err != nil && !os.IsNotExist(err) {
+		return "", fmt.Errorf("failed to read seccomp profile file %s: %w", seccompPath, err)
+	}
+	if string(curBytes) == seccompProfile {
+		return seccompPath, nil // File already exists and matches the expected profile
+	}
+
+	if err := os.WriteFile(seccompPath, []byte(seccompProfile), 0o644); err != nil {
+		return "", fmt.Errorf("failed to write seccomp profile to %s: %w", seccompPath, err)
+	}
+	slog.DebugContext(ctx, "created seccomp profile", "path", seccompPath)
+	return seccompPath, nil
+}