Files
Daniel Samson f477ef7d9f iommu: enable Intel VT-d translation with per-claim device confinement
First enforcement step of the IOVA track. A vendor-neutral IOMMU core
(iommu.zig) drives an Intel VT-d backend (iommu-intel.zig) to give DMA a
real translation layer instead of the fail-open free-for-all M16 left.

- Boot posture is now stated explicitly: "iommu online (Intel VT-d)"
  with version/agaw/rmrr, or "none present - DMA fail-open (unisolated)".
- DMAR parsing extended to select the INCLUDE_PCI_ALL unit (real Intel
  PCs put an iGPU-scoped unit first) and record single-path-endpoint
  RMRRs; multi-hop scopes and extra DRHDs are counted and warned, never
  silently dropped.
- Translation is enabled at boot into a blanket identity domain (all RAM
  + RMRRs, 2 MiB leaves). PCI functions are enumerated post-boot by the
  ring-3 pci-bus driver, so a device is attached to the domain when its
  driver claims it (confineDevice, with claim rollback if confinement
  fails) and detached on driver death, before broker release and DMA
  frame teardown. Unclaimed devices are non-present: their DMA faults.
- Interrupt remapping stays off, so MSI writes to 0xFEE00000 bypass
  translation and the interrupt-driven xHC keeps working.
- devices-broker gains pciAddressOf (derives BDF from the config-space
  ECAM offset), unclaim, and forEachPciFunction.

Faults are drained and logged rate-limited as DANOS-IOMMU-FAULT.

Cases: iommu extended (translation on, scratch-domain map/resolve/unmap,
zero idle faults); new iommu-usb-storage and iommu-usb-hid run the full
storage + input stacks through translated DMA with MSI intact. 103/103.
2026-07-26 18:31:27 +01:00

561 lines
31 KiB
Zig

const std = @import("std");
const boot_handoff = @import("boot-handoff");
const abi = @import("abi");
const parameters = @import("parameters");
const architecture = @import("architecture");
const console = @import("console.zig");
const log = @import("log.zig");
const wall_clock = @import("wall-clock.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const scheduler = @import("scheduler.zig");
const sync = @import("sync.zig");
const process = @import("process.zig");
const devices_broker = @import("devices-broker.zig");
const irq = @import("irq.zig");
const iommu = @import("iommu.zig");
const platform = @import("platform");
const tests = @import("tests.zig");
const build_options = @import("build_options");
const BootInformation = boot_handoff.BootInformation;
/// The calling convention used to enter the kernel. Pinned to SystemV explicitly:
/// the bootloader is built for the UEFI target, whose C convention is Microsoft
/// x64 (first argument in RCX), while the kernel is SystemV (first argument in
/// RDI). Both sides reference this so the `boot_information` pointer lands in the
/// register the other expects. `boot_handoff.kernel_abi` re-exports it to the loader.
pub const kernel_abi = boot_handoff.kernel_abi;
// POST/checkpoint codes emitted to I/O port 0x80 at boot milestones — the
// last-resort progress signal on a machine with no text output at all.
const cp_entry = 0x10;
const cp_paging = 0x20;
const cp_heap = 0x30;
const cp_discovery = 0x40;
const cp_scheduler = 0x50;
const cp_timer = 0x60;
const cp_running = 0x70;
const cp_exception = 0xE0;
const cp_panic = 0xEE;
/// Physical address of the low page reserved at boot for the AP trampoline (0 = none
/// was available). Claimed right after the frame allocator comes up, before paging
/// and the heap consume the scarce sub-1 MiB frames.
var ap_trampoline_page: u64 = 0;
/// Kernel entry point. The bootloader jumps here after `ExitBootServices` with a
/// pointer to the handoff data. There is no runtime, no stack unwinding, and no
/// caller to return to, so this never returns.
/// The real entry (`_start`, in isr.s) installs a kernel-owned stack in .bss
/// then calls this with the loader's `boot_information` pointer in RDI. We can't keep
/// running on the loader's stack: it's a low physical address that the identity
/// map covers only transitionally, and vanishes once the kernel drops the low
/// half. `boot_information` (also low) is reached through the physmap — its base is the
/// same under the loader's bootstrap tables and the kernel's own.
export fn kmainEntry(boot_information: *const BootInformation) callconv(kernel_abi) noreturn {
kmain(@ptrFromInt(boot_handoff.physicalToVirtual(@intFromPtr(boot_information))));
}
fn kmain(boot_information: *const BootInformation) noreturn {
// The **log** is the machine-readable diagnostic stream: it fans out to every
// *diagnostic* channel that exists (serial, the 0xE9 debug console, and later a
// file on a ramdisk/USB/SSD), so a message survives as long as any is present.
// A headless, serial-less machine still boots correctly — it just goes quiet,
// with port-0x80 checkpoints as the only progress signal.
//
// Serial is compiled in only under -Dserial (build.zig): a real machine often
// has no live legacy COM1, and the log survives in the RAM buffer (below) and
// is flushed to disk — so serial is now a QEMU/dev convenience the flashable
// image leaves out. When it *is* built in, `serialInit`'s loopback probe still
// guards against a dead port (so a -Dserial image is safe on real hardware).
if (build_options.serial) {
architecture.serialInit();
log.addSink(architecture.serialWrite);
}
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
// Retain the whole stream in a RAM buffer too, so a user program can later
// read it back (klog_read) and persist the boot log to disk — the only way to
// see it on a headless/real machine with no host capturing serial.
// (Retention is the tagged ring inside log.zig — not a sink.)
// The **framebuffer** is deliberately *not* a log sink. It's a separate output
// surface — a bootstrap text console today, a graphics device driver later — so
// we never assume the OS is text-based. Only a few user-facing status lines
// (via `status`) and panics are mirrored to it; the verbose log stays out.
//
// The console is brought up *after* paging (below), not here: its one-time
// full-screen clear then runs on the kernel's **write-combining** mapping of the
// framebuffer instead of the loader's uncached one — a fast burst rather than
// millions of uncached writes on real hardware. Until then, on-screen output is
// absent (an early panic still lands in the serial/RAM log); the trade is worth
// a near-instant boot. `console.write` is a safe no-op while the console is down.
const fb = boot_information.framebuffer;
log.checkpoint(cp_entry);
// Catch CPU exceptions before doing anything that might fault: install our
// reporter, then bring up the GDT + IDT.
architecture.setFaultHandler(onException);
architecture.init();
status("/system/kernel: initialising kernel...\n");
if (build_options.serial) log.write(if (architecture.serialPresent())
"/system/kernel: serial console online (COM1)\n"
else
"/system/kernel: no serial UART (COM1 absent) -> log kept in RAM/debugcon\n");
log.write("/system/kernel: cpu tables online (GDT, IDT, TSS)\n");
log.print(" resolution : {d}x{d}\n", .{ fb.width, fb.height });
log.print(" pitch : {d} bytes\n", .{fb.pitch});
log.print(" format : {s}\n", .{@tagName(fb.format)});
log.print(" framebuffer: 0x{x:0>16}\n", .{fb.base});
log.print(" footprint : {d} MiB\n", .{(fb.pitch * fb.height) / (1024 * 1024)});
// Summarise the physical memory the loader handed us. The array is danos's
// own MemoryRegion, so this is a plain slice — no firmware layout in sight.
const regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.memory_map.regions)))[0..boot_information.memory_map.len];
var usable_pages: u64 = 0;
var reserved_pages: u64 = 0; // reserved RAM only — MMIO is device space, not RAM
for (regions) |r| {
switch (r.kind) {
.usable => usable_pages += r.pages,
.reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages,
.mmio => {},
}
}
const total_pages = usable_pages + reserved_pages;
const total_bytes = total_pages * abi.page_size;
const gib = 1 << 30;
log.write("\n/system/kernel: physical memory\n");
log.print(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) });
log.print(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)});
log.print(" reserved : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)});
log.print(" regions : {d} - entries in the firmware memory map\n", .{regions.len});
// Bring up the physical frame allocator over that map, and prove it works:
// allocate three frames, then hand them back.
pmm.init(boot_information.memory_map);
// Claim the AP trampoline's low (<1 MiB) page *now*, before paging and the heap
// draw down sub-1 MiB frames (the allocator scans upward from frame 0). Held
// until SMP bring-up; 0 means none was available (we stay uniprocessor).
ap_trampoline_page = pmm.allocBelow(0x100000) orelse 0;
const s1 = pmm.stats();
log.print("\n/system/kernel: frame allocator online\n", .{});
log.print(" free frames: {d} ({d} MiB)\n", .{ s1.free_frames, mib(s1.free_frames) });
const f0 = pmm.alloc();
const f1 = pmm.alloc();
const f2 = pmm.alloc();
log.print(" alloc x3 : 0x{x} 0x{x} 0x{x}\n", .{ f0 orelse 0, f1 orelse 0, f2 orelse 0 });
if (f0) |p| pmm.free(p);
if (f1) |p| pmm.free(p);
if (f2) |p| pmm.free(p);
log.print(" after free : {d} frames free\n", .{pmm.stats().free_frames});
// Switch off the firmware's page tables onto our own (with real permissions).
architecture.enablePaging(pmm.alloc, pmm.free, boot_information);
log.checkpoint(cp_paging);
log.print("\n/system/kernel: paging enabled\n", .{});
log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});
// Now on our own tables, the framebuffer window is write-combining: bring up the
// on-screen console and clear it to a blank canvas (a fast burst here, not the loader's
// uncached crawl). Routine boot output goes only to the log; this console now exists for
// early-boot and fatal (`fatal`/panic) output, until the display service takes over.
console.init(fb);
// The console joins the log sinks: the boot transcript — kernel AND
// userspace lines, each timestamped by the renderer — shows on screen
// until the display service claims the framebuffer (which flips the
// console's `suppressed` and silences this sink). On a machine with no
// serial this is the only live view of the boot, and a slow boot becomes
// diagnosable by eye: the timeline is right there.
if (console.present()) log.addSink(console.write);
log.write(if (console.present())
"/system/kernel: framebuffer ready (early-boot + fatal fallback; the display service drives it in normal operation)\n"
else
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
// Bring up the kernel heap (dynamic allocation), built on the VMM.
heap.init();
log.checkpoint(cp_heap);
log.write("\n/system/kernel: kernel heap online\n");
// Measure the amount of resources the kernel is actually using
const s2 = pmm.stats();
log.print(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)});
// Enumerate hardware from the firmware tables (ACPI here) into a generic
// device tree, then list it. Discovery walks ACPI memory directly (identity-
// mapped) and maps PCIe configuration space on demand via the VMM. A failure here is
// not fatal yet — log it and carry on.
const hal = platform.Hal{
.mapMmio = architecture.mapMmio,
.pioRead = architecture.pioRead,
.pioWrite = architecture.pioWrite,
};
if (platform.discover(boot_information, heap.allocator(), hal)) |devtree| {
var device_tree = devtree;
log.write("\n/system/kernel: device discovery online\n");
device_tree.dump(log.write);
// Snapshot the device tree for user-space drivers (device_enumerate/claim/
// mmio_map operate on this flat, id-indexed table + claim map).
devices_broker.init(&device_tree);
if (devices_broker.dropped > 0) {
// Otherwise entirely silent: drivers would just never see that hardware.
log.print("/system/kernel: WARNING {d} device(s) dropped — table full\n", .{devices_broker.dropped});
}
// Publish the loader's framebuffer as a claimable `display` device, so a
// user-space display service can take it over the same claim + mmio_map path as
// any other hardware (it is not firmware-discovered; it rides the boot handoff).
if (devices_broker.seedDisplay(fb.base, fb.width, fb.height, fb.pitch, @intFromEnum(fb.format), fb.refresh_hz)) |display_id| {
log.print("/system/kernel: framebuffer device {d} seeded ({d}x{d}, pitch {d}, {d} Hz, write-combining)\n", .{ display_id, fb.width, fb.height, fb.pitch, fb.refresh_hz });
}
// Install the device-IRQ trampolines, so a driver's irq_bind has vectors to
// land on. Every line stays masked until something binds it (ioapic.init).
irq.init();
// Bring up DMA translation: build the IOMMU domains and enable it (or record
// fail-open when no unit exists). Must run before any driver claims a device —
// an unclaimed device's DMA is blocked once translation is on. Logs its own
// enable block; the fail-open posture is stated in the platform block below.
iommu.init();
// Power register map, from the FADT (the SLP_TYP sleep values live in AML,
// which the kernel doesn't parse — the ring-3 acpi service owns soft-off).
const pw = platform.powerInformation();
log.write("/system/kernel: power\n");
log.print(" pm1a_cnt : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width });
log.print(" reset : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value });
// Feed the architecture layer the discovered addresses/facts so it makes no legacy
// assumptions — the point of all this on UEFI Class 3 firmware. MMIO bases
// (HPET, I/O APIC) come from the device tree; scalar facts from ACPI.
const pinfo = platform.platformInformation();
const hpet_base: u64 = if (device_tree.firstOfClass(.timer)) |t|
(if (t.firstResource(.memory)) |r| r.start else 0)
else
0;
var ioapic_base: u64 = 0;
var ioapic_gsi: u32 = 0;
if (device_tree.firstOfClass(.interrupt_controller)) |ic| {
if (ic.firstResource(.memory)) |r| ioapic_base = r.start;
if (ic.firstResource(.irq)) |r| ioapic_gsi = @intCast(r.start);
}
var isos: [16]architecture.IsoEntry = undefined;
const iso_n = @min(pinfo.override_count, isos.len);
for (0..iso_n) |i| isos[i] = .{
.source = pinfo.overrides[i].source,
.gsi = pinfo.overrides[i].gsi,
.flags = pinfo.overrides[i].flags,
};
const pm_timer: ?architecture.PmTimer = if (pinfo.pm_timer.present())
.{ .mmio = pinfo.pm_timer.mmio, .address = pinfo.pm_timer.address, .is_32bit = pinfo.pm_timer_32bit }
else
null;
architecture.configurePlatform(.{
.pic_present = pinfo.pic_present,
.hpet_base = hpet_base,
.pm_timer = pm_timer,
.ioapic_base = ioapic_base,
.ioapic_gsi_base = ioapic_gsi,
.overrides = isos[0..iso_n],
});
if (pinfo.spcr_uart) |u| architecture.serialReconfigure(u.mmio, u.address);
log.write("/system/kernel: platform\n");
log.print(" 8259 PIC : {s}\n", .{if (pinfo.pic_present) "present" else "absent"});
log.print(" lapic base : 0x{x}\n", .{pinfo.lapic_base});
log.print(" hpet base : 0x{x}\n", .{hpet_base});
log.print(" pm timer : {s} 0x{x} ({s})\n", .{ if (pinfo.pm_timer.mmio) "mmio" else "io", pinfo.pm_timer.address, if (pinfo.pm_timer_32bit) "32-bit" else "24-bit" });
if (pinfo.spcr_uart) |u| {
log.print(" console UART: {s} 0x{x} (SPCR type {d})\n", .{ if (u.mmio) "mmio" else "io", u.address, pinfo.spcr_kind });
} else {
log.write(" console UART: none in SPCR -> legacy COM1\n");
}
log.print(" ioapic : base 0x{x}, {d} inputs (masked); route0 raw 0x{x}\n", .{ ioapic_base, architecture.irqRouteCount(), architecture.irqRouteRaw(0) });
const cores = platform.cpus();
log.print(" cpus : {d} usable core(s); 1 running (BSP), {d} AP(s) parked (SMP bring-up pending)\n", .{ cores.len, if (cores.len > 0) cores.len - 1 else 0 });
if (platform.cpusDropped() > 0)
log.print(" cpus : WARNING {d} core(s) beyond pool cap dropped\n", .{platform.cpusDropped()});
// The DMA-isolation posture, stated plainly at every boot. When a unit exists,
// iommu.init already logged its enable block above; here we only state the
// fail-open case, so a boot without the line is a boot with translation on.
if (!iommu.enabled())
log.write(" iommu : none present - DMA fail-open (unisolated)\n");
} else |err| {
log.print("\n/system/kernel: device discovery failed: {s}\n", .{@errorName(err)});
}
log.checkpoint(cp_discovery);
// Install the system_call handler (int 0x80 gate + system_call stub) once, before any
// user code runs.
process.init();
// Register the current context as the first task before enabling preemption.
scheduler.init(4);
log.checkpoint(cp_scheduler);
log.write("\n/system/kernel: scheduler online\n");
// Start the timer and unmask interrupts — the kernel now has a heartbeat, and
// the timer preempts among tasks.
architecture.startTimer();
architecture.enableInterrupts();
log.checkpoint(cp_timer);
log.print("/system/kernel: timer online ({d} Hz tick; timer clock {d} MHz, clock {d} MHz; calibrated via {s})\n", .{ architecture.timer_hz, architecture.timerClockHz() / 1_000_000, architecture.clockHz() / 1_000_000, architecture.timerCalibrationSource() });
// The tsc-sync test forces the TSC clocksource on before the cores come up, so the
// TSC + warp-check path is exercised even under TCG (which won't advertise an
// invariant TSC). Inert in a normal build (docs/timers.md).
if (build_options.test_case) |tc| {
if (std.mem.eql(u8, tc, "tsc-sync")) architecture.forceTscClocksourceForTest();
}
// Wake the other cores (application processors). A no-op on a single-core
// machine; on SMP each AP climbs to long mode and reports in (docs/smp.md). The
// per-core TSC warp check rides this: each AP is vetted before it joins the run
// loop (docs/timers.md).
bringUpSecondaries();
// Report the monotonic clock's final reliability, now the warp check has run on
// every core. On real Intel/AMD this is the invariant, synchronized TSC; a bare
// VM (no invariant bit) or a machine whose cores' TSCs skew uses the HPET instead.
log.print("/system/kernel: clocksource {s} (TSC invariant: {s}, synchronized: {s})\n", .{
architecture.clockSourceName(),
if (architecture.clockInvariant()) "yes" else "no",
if (architecture.clockSynchronized()) "yes" else "no",
});
if (!architecture.clockSynchronized())
log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n");
// Anchor wall-clock time: read the RTC once, now the monotonic clock is final.
wall_clock.init();
log.print("/system/kernel: wall clock {d} (Unix epoch seconds, UTC, from the RTC)\n", .{wall_clock.nowSeconds()});
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
// Normal builds fall through to the idle halt.
if (build_options.test_case) |case| {
tests.run(case, boot_information);
architecture.halt();
}
log.checkpoint(cp_running);
status("/system/kernel: initialised.\n");
// Publish the initial-ramdisk so user space can `system_spawn` its bundled
// binaries by name. The kernel no longer launches them itself: init is the
// service supervisor and the device manager spawns the drivers it discovers.
publishInitialRamdisk(boot_information);
// Hand over to user space: spawn /system/services/init out of the ramdisk as a
// real ring-3 process, PID 1 — it rides the same table as every other binary.
// As the supervisor it brings up the system services (the VFS server, the device
// manager); the device manager then discovers the hardware and spawns each
// driver. init runs on its own address space, preemptively — this boot context
// becomes the BSP's idle loop.
status("/system/kernel: starting /system/services/init...\n");
process.spawnBundled("/system/services/init") catch |err| {
statusPrint("/system/kernel: /system/services/init failed to start: {s}\n", .{@errorName(err)});
};
// Become the idle task: drop below every real task and halt until an
// interrupt. The timer keeps preempting into init and any other work.
scheduler.setPriority(0);
status("\n/system/kernel: kernel idle; user space is running.\n");
architecture.halt();
}
/// Publish the initial-ramdisk image to the process layer so user space can
/// `system_spawn` its bundled binaries by name. The kernel used to spawn every
/// bundled program here; now init (the service supervisor) and the device manager
/// (drivers) own that, so this only hands the image over — nothing is launched from
/// the kernel.
fn publishInitialRamdisk(boot_information: *const boot_handoff.BootInformation) void {
if (boot_information.initial_ramdisk_len == 0) return;
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
process.setInitialRamdisk(image);
}
/// Wake the application processors the firmware left parked. Allocates the low
/// trampoline page (and makes it executable), then wakes each non-boot core in turn,
/// handing it a fresh kernel stack and its per-CPU slot. Cores that don't report in
/// are left parked — the running system is unaffected. See docs/smp.md.
fn bringUpSecondaries() void {
const cores = platform.cpus();
if (cores.len <= 1) return;
// A low (<1 MiB) frame was reserved at boot for the real-mode trampoline (a SIPI
// vector addresses it). It's kept for the system's life — armed only during a
// wake, inert (zeroed, non-executable) otherwise — so cores can be re-woken later.
if (ap_trampoline_page == 0) {
log.write("/system/kernel: smp: no low page for the AP trampoline; staying uniprocessor\n");
return;
}
architecture.setTrampolinePage(ap_trampoline_page);
architecture.setSecondaryEntry(scheduler.secondaryMain); // where a woken core joins the run loop
// Test hook: the smp-retry case forces the first wake to fail, so the retry below
// must still bring every core online. Inert in a normal build (test_case is null).
if (build_options.test_case) |tc| {
if (std.mem.eql(u8, tc, "smp-retry")) architecture.testFailNextWakes(1);
}
log.print("\n/system/kernel: bringing up {d} application processor(s)\n", .{cores.len - 1});
const maximum_wake_attempts = 3; // a core that misses the first INIT-SIPI-SIPI gets retried
for (cores[1..], 1..) |core, index| {
const stack = heap.allocator().alloc(u8, parameters.kernel_stack_size) catch {
log.print(" cpu apic_id {d}: no stack; skipped\n", .{core.apic_id});
continue;
};
const stack_top = (@intFromPtr(stack.ptr) + stack.len) & ~@as(usize, 15);
// This core's dedicated fault stack — allocated only now that the core is
// real, rather than reserved statically for every possible core.
const fault_stack = heap.allocator().alloc(u8, architecture.fault_stack_size) catch {
log.print("/system/kernel: cpu apic_id {d}: no fault stack; skipped\n", .{core.apic_id});
continue;
};
architecture.setFaultStack(index, (@intFromPtr(fault_stack.ptr) + fault_stack.len) & ~@as(usize, 15));
const pc = scheduler.prepareSecondary(index, core.apic_id);
var attempt: u32 = 1;
while (attempt <= maximum_wake_attempts) : (attempt += 1) {
if (architecture.startSecondary(core.apic_id, stack_top, @intFromPtr(pc), index)) {
pc.online = true;
log.print("/system/kernel: cpu apic_id {d}: online (attempt {d})\n", .{ core.apic_id, attempt });
break;
}
if (attempt == maximum_wake_attempts)
log.print("/system/kernel: cpu apic_id {d}: no response after {d} attempts (parked)\n", .{ core.apic_id, maximum_wake_attempts });
}
}
log.print("/system/kernel: {d}/{d} cores online\n", .{ scheduler.onlineCount(), cores.len });
}
/// A user-facing status line. Now that the user-space **display service** owns the
/// framebuffer in normal operation (docs/display.md), routine kernel output goes to the
/// diagnostic `log` (serial/debugcon/RAM) *only* — never to the on-screen console, which
/// the compositor is about to paint over. For a message that must reach the screen even so
/// — a panic or a fatal fault, when the machine is going down — use `fatal`.
fn status(message: []const u8) void {
log.write(message);
}
/// A fatal, user-facing message: to the diagnostic log *and* the on-screen console, forcing
/// the console back on (`setSuppressed(false)`) first — a dying machine's last words outrank
/// any display service holding the framebuffer. The console is otherwise silent in normal
/// operation (see `status`); it exists now only for early-boot and fatal output.
fn fatal(message: []const u8) void {
log.appendPanic(message); // bounded lock wait: a panic never deadlocks on the log
console.setSuppressed(false);
console.write(message);
}
fn statusPrint(comptime fmt: []const u8, args: anytype) void {
var buffer: [256]u8 = undefined;
status(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}
fn fatalPrint(comptime fmt: []const u8, args: anytype) void {
var buffer: [256]u8 = undefined;
fatal(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}
/// Frames (4 KiB pages) to whole MiB.
fn mib(pages: u64) u64 {
return pages * abi.page_size / (1024 * 1024);
}
fn kib(frames: u64) u64 {
return frames * abi.page_size / (1024);
}
/// 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.)
/// Classify a CPU exception vector as the ExitReason a supervisor reads — the
/// fault classes of docs/process-lifecycle.md. Faults are exit reasons, never
/// signals delivered to the faulting process: recovery is restart, not a handler.
fn exitReasonForVector(vector: u64) abi.ExitReason {
return switch (vector) {
14 => .segmentation_fault, // page fault
6 => .illegal_instruction, // invalid opcode
0, 16, 19 => .arithmetic_fault, // divide error, x87, SIMD
13 => .protection_fault, // general protection
else => .fault,
};
}
fn onException(state: *const architecture.CpuState) noreturn {
if (architecture.fromUser(state) and scheduler.currentIsUserProcess() and recoverableFault(state.vector)) {
statusPrint("\n/system/kernel: process {d} ({s}) killed by {s} (vector {d}) on core {d}\n", .{ scheduler.currentId(), scheduler.current().name(), 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(exitReasonForVector(state.vector)); // reclaims everything, reschedules; never returns
}
log.checkpoint(cp_exception);
const core = scheduler.currentCpuIndex();
// The machine is going down: paint the exception on screen too — `fatalPrint` forces the
// console back on even if a display service was holding the framebuffer — on top of the
// diagnostic log.
fatalPrint("\nCPU EXCEPTION on core {d}: {s} (vector {d})\n", .{ core, architecture.exceptionName(state.vector), state.vector });
// Name the culprit: which task, and whether it faulted in ring 3 (a process the
// kernel would normally kill — landing here means it had no address space) or ring 0
// (the trusted base itself). Without this the fatal report is anonymous.
fatalPrint(" task : {d} ({s}), {s}\n", .{ scheduler.currentIdSafe(), scheduler.currentNameSafe(), if (architecture.fromUser(state)) "ring 3 (user)" else "ring 0 (kernel)" });
fatalPrint(" error code : 0x{x}\n", .{state.error_code});
fatalPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
fatalPrint(" SP : 0x{x:0>16}\n", .{architecture.stackPointer(state)});
if (architecture.faultAddress(state)) |address| fatalPrint(" fault addr : 0x{x:0>16}\n", .{address});
var buffer: [128]u8 = undefined;
log.recordPanic(std.fmt.bufPrint(&buffer, "CPU exception {s} (vector {d}) on core {d} at IP 0x{x}", .{ architecture.exceptionName(state.vector), state.vector, core, architecture.instructionPointer(state) }) catch "cpu exception");
// Free the BKL if this core held it (a kernel-mode fault, or a nested fault in the
// recovery teardown), so halting this one core doesn't deadlock every other core on
// the lock. Only that core stops; the rest — and the supervisor — keep running.
sync.releaseIfHeldHere();
architecture.halt();
}
/// Freestanding has no OS to receive a panic. Emit it to every output sink, drop a
/// POST code + a persistent breadcrumb (so a post-mortem can recover it even with
/// no live console), then halt. Assumes no console — the sinks self-guard.
pub const panic = std.debug.FullPanic(struct {
fn panic(message: []const u8, first_trace_address: ?usize) noreturn {
_ = first_trace_address;
log.checkpoint(cp_panic);
log.recordPanic(message);
fatal("\nKERNEL PANIC: "); // a panic outranks any display service holding the screen
fatal(message);
fatal("\n");
fatalPrint(" task : {d} ({s})\n", .{ scheduler.currentIdSafe(), scheduler.currentNameSafe() });
sync.releaseIfHeldHere(); // don't deadlock the other cores on the lock we may hold
architecture.halt();
}
}.panic);