Add safety guardrails for Force Kill and protected system processes

Security improvements:
- Force Kill (SIGKILL) now always requires confirmation - removed "Don't ask
  again" option for this destructive operation
- Added protection for critical system processes (kernel_task, launchd,
  WindowServer, loginwindow) - UI prevents force-killing these
- Terminate (SIGTERM) retains the "Don't ask again" option as it's a gentler
  operation that allows processes to save data

UX improvements:
- Clear error message when attempting to force-kill protected processes
- ConfirmationSheet now supports optional "Don't ask again" toggle via
  nullable skipPreferenceKey parameter

This prevents accidental system crashes from force-killing critical processes
while maintaining power-user convenience for regular termination.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hariel1985
2026-01-31 23:26:46 +01:00
szülő 02a08d23cc
commit 9292166223

Fájl megtekintése

@@ -22,15 +22,21 @@ struct ProcessView: View {
@State private var showForceKillConfirm = false @State private var showForceKillConfirm = false
@State private var processToKill: ProcessItem? @State private var processToKill: ProcessItem?
// "Don't ask again" preferences // "Don't ask again" preference (only for Terminate, not Force Kill)
@AppStorage("skipTerminateConfirm") private var skipTerminateConfirm = false @AppStorage("skipTerminateConfirm") private var skipTerminateConfirm = false
@AppStorage("skipForceKillConfirm") private var skipForceKillConfirm = false
// Error handling // Error handling
@State private var showErrorAlert = false @State private var showErrorAlert = false
@State private var errorTitle = "" @State private var errorTitle = ""
@State private var errorMessage = "" @State private var errorMessage = ""
// Protected system processes that cannot be force-killed via UI
private static let protectedProcesses = ["kernel_task", "launchd", "WindowServer", "loginwindow"]
private func isProtectedProcess(_ name: String) -> Bool {
Self.protectedProcesses.contains(name)
}
private func updateDisplayedProcesses() { private func updateDisplayedProcesses() {
let filtered = monitor.processes.filter { let filtered = monitor.processes.filter {
searchText.isEmpty || searchText.isEmpty ||
@@ -248,7 +254,8 @@ struct ProcessView: View {
message: "Are you sure you want to force kill \"\(process.name)\" (PID: \(process.pid))?\n\nThis will immediately terminate the process without allowing it to save data.", message: "Are you sure you want to force kill \"\(process.name)\" (PID: \(process.pid))?\n\nThis will immediately terminate the process without allowing it to save data.",
actionTitle: "Force Kill", actionTitle: "Force Kill",
isDestructive: true, isDestructive: true,
skipPreferenceKey: "skipForceKillConfirm", // SAFETY: Force Kill always requires confirmation - no "Don't ask again" option
skipPreferenceKey: nil,
onConfirm: { onConfirm: {
performForceKill(process: process) performForceKill(process: process)
}, },
@@ -276,13 +283,18 @@ struct ProcessView: View {
private func initiateForceKill() { private func initiateForceKill() {
guard let process = selectedProcessItem else { return } guard let process = selectedProcessItem else { return }
processToKill = process
if skipForceKillConfirm { // SAFETY: Prevent force-killing critical system processes
performForceKill(process: process) if isProtectedProcess(process.name) {
} else { errorTitle = "Cannot Force Kill System Process"
showForceKillConfirm = true errorMessage = "\"\(process.name)\" is a critical system process.\n\nForce killing this process would crash your system or cause immediate logout."
showErrorAlert = true
return
} }
processToKill = process
// SAFETY: Force Kill always requires confirmation - no "Don't ask again" option
showForceKillConfirm = true
} }
private func performTerminate(process: ProcessItem) { private func performTerminate(process: ProcessItem) {
@@ -354,7 +366,7 @@ struct ConfirmationSheet: View {
let message: String let message: String
let actionTitle: String let actionTitle: String
let isDestructive: Bool let isDestructive: Bool
let skipPreferenceKey: String let skipPreferenceKey: String? // nil = don't show "Don't ask again" option
let onConfirm: () -> Void let onConfirm: () -> Void
let onCancel: () -> Void let onCancel: () -> Void
@@ -367,7 +379,7 @@ struct ConfirmationSheet: View {
message: String, message: String,
actionTitle: String, actionTitle: String,
isDestructive: Bool, isDestructive: Bool,
skipPreferenceKey: String, skipPreferenceKey: String? = nil,
onConfirm: @escaping () -> Void, onConfirm: @escaping () -> Void,
onCancel: @escaping () -> Void onCancel: @escaping () -> Void
) { ) {
@@ -378,7 +390,8 @@ struct ConfirmationSheet: View {
self.skipPreferenceKey = skipPreferenceKey self.skipPreferenceKey = skipPreferenceKey
self.onConfirm = onConfirm self.onConfirm = onConfirm
self.onCancel = onCancel self.onCancel = onCancel
self._skipConfirm = AppStorage(wrappedValue: false, skipPreferenceKey) // Use a dummy key if skipPreferenceKey is nil (won't be used anyway)
self._skipConfirm = AppStorage(wrappedValue: false, skipPreferenceKey ?? "_unused_")
} }
var body: some View { var body: some View {
@@ -396,8 +409,11 @@ struct ConfirmationSheet: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
// Only show "Don't ask again" if a preference key is provided
if skipPreferenceKey != nil {
Toggle("Don't ask again", isOn: $dontAskAgain) Toggle("Don't ask again", isOn: $dontAskAgain)
.toggleStyle(.checkbox) .toggleStyle(.checkbox)
}
HStack(spacing: 12) { HStack(spacing: 12) {
Button("Cancel") { Button("Cancel") {
@@ -407,7 +423,7 @@ struct ConfirmationSheet: View {
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Button(actionTitle) { Button(actionTitle) {
if dontAskAgain { if dontAskAgain && skipPreferenceKey != nil {
skipConfirm = true skipConfirm = true
} }
onConfirm() onConfirm()