Kill a faulting user process instead of halting the machine

A CPU exception raised in ring 3 by a scheduled process now kills that
process - IRQ bindings, IPC handles, and address space reclaimed, a
client it owed a reply to failed with the new -EPEER instead of hung -
and the core reschedules (docs/resilience.md step 2). Kernel-mode
faults, NMI, double fault, and machine check stay terminal, as does the
borrowed-thread isolation probe. Proven by the new fault-recovery QEMU
test: init keeps heartbeating after a process page-faults to death.
This commit is contained in:
Daniel Samson
2026-07-11 04:57:02 +01:00
parent 59104dd988
commit 6b3ae0c997
8 changed files with 198 additions and 35 deletions
+35 -6
View File
@@ -385,13 +385,42 @@ fn kib(frames: u64) u64 {
return frames * abi.page_size / (1024);
}
/// Report a CPU exception and halt **this core**. There's no fault recovery yet, so
/// the faulting core is terminal — but the fault is *contained* to it: on an
/// application processor only that core stops, and the rest of the system keeps
/// running (full recovery — kill the task, keep the core — is the resilience track,
/// see docs/resilience.md). The report names the core so an AP fault is attributed,
/// and goes to every output sink plus a POST code and a persistent breadcrumb.
/// Whether a ring-3 exception is attributable to the process that raised it — and
/// therefore recoverable by killing that process. NMI (2), double fault (8), and
/// machine check (18) report machine or kernel trouble even when they arrive with a
/// user CS (an NMI interrupts whatever happens to be running), so they stay terminal.
fn recoverableFault(vector: u64) bool {
return switch (vector) {
2, 8, 18 => false,
else => true,
};
}
/// Report a CPU exception. Two outcomes (docs/resilience.md):
///
/// **A fault taken in user mode kills the faulting process, not the machine.** The
/// kernel is intact — the CPU trapped onto the task's kernel stack — so the process
/// is killed, everything it held (address space, IRQ bindings, IPC handles, device
/// grants' frames) is reclaimed, and the core reschedules. A crashing driver takes
/// itself down, never the OS.
///
/// **Everything else halts this core.** A kernel-mode fault means the trusted base
/// itself is broken — there is nothing safe to kill — and NMI/#DF/#MC report machine
/// trouble regardless of CS (`recoverableFault`). Even then the fault is *contained*:
/// on an application processor only that core stops and the rest keep running. The
/// report names the core so an AP fault is attributed, and goes to every output sink
/// plus a POST code and a persistent breadcrumb. (A ring-3 fault on a *borrowed*
/// kernel thread — process.run, the user-pf isolation probe — also lands here: there
/// is no scheduled process to kill.)
fn onException(state: *const architecture.CpuState) noreturn {
if (architecture.fromUser(state) and scheduler.currentIsUserProcess() and recoverableFault(state.vector)) {
statusPrint("\ndanos: process {d} killed by {s} (vector {d}) on core {d}\n", .{ scheduler.currentId(), architecture.exceptionName(state.vector), state.vector, scheduler.currentCpuIndex() });
statusPrint(" error code : 0x{x}\n", .{state.error_code});
statusPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
if (architecture.faultAddress(state)) |address| statusPrint(" fault addr : 0x{x:0>16}\n", .{address});
process.killCurrentProcess(); // reclaims everything, reschedules; never returns
}
log.checkpoint(cp_exception);
const core = scheduler.currentCpuIndex();
// A fault is user-facing enough to paint on screen too (via statusPrint), on