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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user