212 lines
8.9 KiB
Zig
212 lines
8.9 KiB
Zig
//! /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;
|
|
}
|