//! /system/drivers/bus — a user-space **bus driver**, and the smallest honest example of one. //! //! A bus driver owns a device that *contains other devices*, enumerates them by some //! bus-specific protocol, and publishes each one into the kernel's device table so a //! class driver can claim it. PCI walks configuration space; USB walks hub descriptors. Here //! the "bus" is the HPET's register block and the "devices" are its comparators, each //! a 0x20-byte window at 0x100 + 0x20*n that can be driven independently. //! //! It's a toy bus, but nothing about the mechanism is: `bus` reads how many children //! exist from the hardware (GENERAL_CAP bits [12:8]), publishes one `DeviceDescriptor` per //! child with a sub-window of its own MMIO plus the shared IRQ, and the kernel checks //! every one of those resources is contained in what `bus` was granted. A comparator //! driver then claims a child and maps only *its* registers — not the whole block. //! //! It also proves the negative: registering a child whose window escapes the parent's //! is refused. Without that check, `device_register` would be a system_call for mapping //! arbitrary physical memory. const std = @import("std"); const runtime = @import("runtime"); const device = runtime.device; const register_general_cap = 0x000; /// Comparator n's registers: configuration+comparator+FSB route, 0x20 bytes. fn timerWindow(hpet_base: u64, n: u64) device.ResourceDescriptor { return .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = hpet_base + 0x100 + 0x20 * n, .len = 0x20, }; } fn findHpet(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor { const total = device.enumerate(buffer); const n = @min(total, buffer.len); for (buffer[0..n]) |d| { if (d.class != @intFromEnum(device.DeviceClass.timer)) continue; if (d.parent != device.no_parent) continue; // the block, not a comparator child for (0..d.resource_count) |j| { if (d.resources[j].kind == @intFromEnum(device.ResourceKind.memory)) return d; } } return null; } /// The parent's MMIO resource, and its IRQ if it has one. fn resourcesOf(d: device.DeviceDescriptor) struct { mmio: device.ResourceDescriptor, irq: ?device.ResourceDescriptor } { var mmio: device.ResourceDescriptor = undefined; var irq: ?device.ResourceDescriptor = null; for (0..d.resource_count) |j| { const r = d.resources[j]; if (r.kind == @intFromEnum(device.ResourceKind.memory)) mmio = r; if (r.kind == @intFromEnum(device.ResourceKind.irq)) irq = r; } return .{ .mmio = mmio, .irq = irq }; } fn firstChildOf(buffer: []device.DeviceDescriptor, total: usize, parent_id: u64) ?u64 { for (buffer[0..@min(total, buffer.len)]) |d| { if (d.parent == parent_id) return d.id; } return null; } pub fn main() void { const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { _ = runtime.system.write("bus: out of memory\n"); return; }; const parent = findHpet(buffer) orelse { _ = runtime.system.write("bus: no HPET\n"); return; }; const resource = resourcesOf(parent); // Claim the bus. Everything below is subdivision of what this claim granted. // // Claims are exclusive, and at a normal boot the kernel spawns every initial_ramdisk // binary — so hpet may own the HPET already. That's not an error, it's the // capability model working: exit quietly and leave the device to its owner. The // `bus` test spawns bus alone, so there it wins the claim. if (!device.claim(parent.id)) { _ = runtime.system.write("bus: HPET already claimed by another driver, nothing to do\n"); return; } // Enumerate the bus: ask the hardware how many children it has. const base = device.mmioMap(parent.id, 0) orelse { _ = runtime.system.write("bus: mmio_map failed\n"); return; }; const cap: *volatile u64 = @ptrFromInt(base + register_general_cap); const n_children = ((cap.* >> 8) & 0x1F) + 1; // Publish one child per comparator, each owning only its own window. var published: u64 = 0; var n: u64 = 0; while (n < n_children) : (n += 1) { var child = std.mem.zeroes(device.DeviceDescriptor); child.class = @intFromEnum(device.DeviceClass.timer); child.pci_class = device.no_pci_class; child.hid_len = 6; child.hid[0..6].* = "hpet-t".*; child.resource_count = 1; child.resources[0] = timerWindow(resource.mmio.start, n); // Comparators share the block's interrupt line; only one child can bind it, // but all of them may legitimately name it. if (resource.irq) |i| { child.resources[child.resource_count] = i; child.resource_count += 1; } if (device.register(parent.id, &child) == null) { _ = runtime.system.write("bus: register failed\n"); return; } published += 1; } // The negative case. A window one byte past the end of the parent's must be // refused — otherwise device_register would be "map any physical page you like". // Confirm the table did not grow, not merely that the call returned null: null // also means NoSpace/BadParent, so a size check is what actually proves the // *containment* rule fired. const before = device.enumerate(buffer); var rogue = std.mem.zeroes(device.DeviceDescriptor); rogue.class = @intFromEnum(device.DeviceClass.unknown); rogue.resource_count = 1; rogue.resources[0] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = resource.mmio.start + resource.mmio.len, .len = 0x1000, }; if (device.register(parent.id, &rogue) != null) { _ = runtime.system.write("bus: FAIL out-of-window child was accepted\n"); return; } if (device.enumerate(buffer) != before) { _ = runtime.system.write("bus: FAIL rogue child leaked into the table\n"); return; } // And confirm the children came back with the right parent and a *narrower* // window than the bus — read from the table, not from our own memory. const total = device.enumerate(buffer); var seen: u64 = 0; for (buffer[0..@min(total, buffer.len)]) |d| { if (d.parent != parent.id) continue; const w = d.resources[0]; if (w.start < resource.mmio.start or w.len >= resource.mmio.len) { _ = runtime.system.write("bus: FAIL child window is not inside the bus\n"); return; } seen += 1; } if (seen != published) { _ = runtime.system.write("bus: FAIL child count mismatch\n"); return; } // Delegation, end to end: claim a child and map *it*. A real class driver would be // a different process; here bus plays both parts, which exercises the same path. // The child's window is 0x20 bytes at parent+0x100, so the register it sees at // offset 0 must be the same timer-0 configuration register the bus sees at 0x100. // // (mmio_map rounds to a page, so the child's mapping physically covers the whole // 4 KiB the HPET lives in — the granularity limit documented in docs/drivers.md. // The *resource* is narrow even though the page isn't.) const child_id = firstChildOf(buffer, device.enumerate(buffer), parent.id) orelse { _ = runtime.system.write("bus: FAIL no child to claim\n"); return; }; if (!device.claim(child_id)) { _ = runtime.system.write("bus: FAIL could not claim own child\n"); return; } const child_base = device.mmioMap(child_id, 0) orelse { _ = runtime.system.write("bus: FAIL child mmio_map refused\n"); return; }; const via_child: *volatile u64 = @ptrFromInt(child_base); const via_bus: *volatile u64 = @ptrFromInt(base + 0x100); if (via_child.* != via_bus.*) { _ = runtime.system.write("bus: FAIL child window does not alias the bus register\n"); return; } // A descriptor pointer into an unmapped page must fail the call, not fault the // kernel. Grab a page, free it, and register through the stale address: if the // kernel dereferenced it raw (rather than copying in through the page tables) this // would triple-fault QEMU and the test would time out instead of printing ok. const scratch = runtime.system.mmap(0x1000, runtime.system.PROT_READ | runtime.system.PROT_WRITE); if (!runtime.system.mmapFailed(scratch)) { _ = runtime.system.munmap(scratch, 0x1000); const descriptor: *const device.DeviceDescriptor = @ptrFromInt(scratch); if (device.register(parent.id, descriptor) != null) { _ = runtime.system.write("bus: FAIL register accepted an unmapped descriptor\n"); return; } } _ = runtime.system.write("bus: ok\n"); while (true) runtime.system.sleep(1000); } pub const panic = runtime.panic; comptime { _ = &runtime.start._start; }