kernel: a refusal names its rule, and two bounds stop failing open
An AMD Ryzen booted to a working compositor with no USB and no storage, and the log said only "register refused". A tree-wide audit of every compile-time ceiling followed: 235 of them, 139 on quantities the machine or a file decides rather than us, 5 documented anywhere, 171 silent when reached. docs/fixed-bounds-audit.md has the inventory. Errno attribution. The errno space was split between the kernel and the envelope, free to drift; it is now one list in system/abi.zig, restated on both sides, with a comptime check in library/device/driver where the two halves are visible. device_register's six refusals and device_claim's three are distinct codes, so a bus driver can say which rule stopped it, and BadParent splits into NoSuchParent and NotYourParent. pci-bus reconciles found against registered instead of counting refused functions as found. Idempotency ordering. The child cap was checked before the identity match, so a restarted bus was refused its own devices — the supervision restart the system leans on ratcheted toward a degraded machine. A re-registration consumes no slot and is now admitted first. IOMMU fail-closed. confineDevice returned success for a device id past the confinement table, leaving the device outside every domain while the caller believed it confined — unreachable only while ids stop at 64, which both the inventory move and a hardware-reported domain count would change. It refuses now, and the coupling to the broker's device cap is a comptime assert rather than a sentence in a comment. PCI apertures. The bridge's MMIO apertures are derived from the holes in the firmware memory map, and the derivation copied sub-4 GiB entries into a fixed [64] array and skipped the rest. A skipped region is not merely lost: the gap finder concludes it is free, so a real machine's 60-200 entry map yields an aperture over live RAM, and containment then admits a child BAR covering kernel memory. Rewritten to walk the map in place, with the hole finder extracted as a pure function and driven by a synthetic 100-entry map in a new test case. Both new tests were verified to fail on the old code. parameters.zig gains the rationale it was missing and loses a stale sentence pointing at the wrong file; vdso.md documents the errno space, including EPEER, which had no written meaning anywhere. docs/os-development/bounds.md is how a ceiling is declared from here. docs/bounds-track-plan.md is the plan to remove the ones we invented. Suite 114 -> 115.
This commit is contained in:
+82
-40
@@ -615,17 +615,78 @@ var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
|
||||
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
|
||||
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
|
||||
/// matter what later moved to user space.
|
||||
pub const AddressRange = struct { base: u64, end: u64 };
|
||||
|
||||
/// The largest holes below 4 GiB in a firmware memory map: the ranges the firmware
|
||||
/// described *nothing* in, which is where a PCI BAR may legitimately live. Fills `out`
|
||||
/// (keeping the largest, replacing the smallest held so far), ignores holes shorter
|
||||
/// than `minimum`, and returns how many entries are non-empty; entries the caller
|
||||
/// reads may be empty and are skipped by `end > base`.
|
||||
///
|
||||
/// **It never copies the map, and that is the point.** The firmware chooses how many
|
||||
/// descriptors its map has — commonly 60–200 on a real machine, 15–25 under OVMF. The
|
||||
/// previous version copied the sub-4 GiB entries into a fixed `[64]` array and
|
||||
/// `continue`d past the rest, which did not merely lose them: a region absent from the
|
||||
/// walk is a region this function concludes is *free*, so a large enough map yields an
|
||||
/// "aperture" lying over live RAM. `device_register` containment would then admit a
|
||||
/// child BAR covering kernel memory, and its claimant could `mmio_map` it. A bound
|
||||
/// whose overflow hands out authority is not a limit; the fix is not a bigger array.
|
||||
///
|
||||
/// Pure, allocation-free, and linear-ish in the map (each inner pass consumes at least
|
||||
/// one region, and it runs once at boot).
|
||||
pub fn largestHolesBelow4G(
|
||||
regions: []const boot_handoff.MemoryRegion,
|
||||
minimum: u64,
|
||||
out: []AddressRange,
|
||||
) usize {
|
||||
const limit: u64 = 1 << 32;
|
||||
for (out) |*hole| hole.* = .{ .base = 0, .end = 0 };
|
||||
|
||||
var cursor: u64 = 0;
|
||||
while (cursor < limit) {
|
||||
// Step over every described region covering the cursor. Regions may overlap
|
||||
// and chain, so repeat until the cursor stops moving.
|
||||
var moved = true;
|
||||
while (moved) {
|
||||
moved = false;
|
||||
for (regions) |region| {
|
||||
if (region.base >= limit) continue;
|
||||
const end = @min(region.base + region.pages * 4096, limit);
|
||||
if (region.base <= cursor and end > cursor) {
|
||||
cursor = end;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cursor >= limit) break;
|
||||
|
||||
// The cursor now sits in a hole; it runs to the next described base, or to
|
||||
// 4 GiB if nothing is described above it.
|
||||
var next: u64 = limit;
|
||||
for (regions) |region| {
|
||||
if (region.base >= limit) continue;
|
||||
if (region.base > cursor and region.base < next) next = region.base;
|
||||
}
|
||||
|
||||
if (next - cursor >= minimum and out.len != 0) {
|
||||
var smallest: usize = 0;
|
||||
for (out, 0..) |hole, i| {
|
||||
if (hole.end - hole.base < out[smallest].end - out[smallest].base) smallest = i;
|
||||
}
|
||||
if (next - cursor > out[smallest].end - out[smallest].base)
|
||||
out[smallest] = .{ .base = cursor, .end = next };
|
||||
}
|
||||
cursor = next;
|
||||
}
|
||||
|
||||
var found: usize = 0;
|
||||
for (out) |hole| {
|
||||
if (hole.end > hole.base) found += 1;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
|
||||
// and tables high), so the holes are the *gaps between* them — a single
|
||||
// "after the last region" rule dies on OVMF's flash at the very top.
|
||||
// Sort-merge the described ranges, then keep the three largest gaps
|
||||
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
|
||||
// high aperture fits). Above 4 GiB one aperture runs from the end of the
|
||||
// described space to the 46-bit line.
|
||||
const Range = struct { base: u64, end: u64 };
|
||||
var below: [64]Range = undefined;
|
||||
var below_count: usize = 0;
|
||||
var high_end: u64 = 1 << 32;
|
||||
for (boot_memory_regions) |region| {
|
||||
const end = region.base + region.pages * 4096;
|
||||
@@ -636,37 +697,18 @@ fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||
// kernel image, the tables, the ramdisk all live there). Bring-up
|
||||
// trust: only the bridge's claimant can register into the aperture.
|
||||
if (region.kind == .usable and end > high_end) high_end = end;
|
||||
if (region.base >= (1 << 32) or below_count == below.len) continue;
|
||||
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
|
||||
below_count += 1;
|
||||
}
|
||||
// Insertion sort by base (the map is small and this runs once at boot).
|
||||
for (1..below_count) |i| {
|
||||
const key = below[i];
|
||||
var j = i;
|
||||
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
|
||||
below[j] = key;
|
||||
}
|
||||
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
|
||||
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
|
||||
var cursor: u64 = 0;
|
||||
var index: usize = 0;
|
||||
while (index <= below_count) : (index += 1) {
|
||||
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
|
||||
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
|
||||
// Keep the three largest, replacing the smallest kept so far.
|
||||
var smallest: usize = 0;
|
||||
for (gaps, 0..) |gap, gi| {
|
||||
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
|
||||
}
|
||||
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
|
||||
gaps[smallest] = .{ .base = cursor, .end = gap_end };
|
||||
}
|
||||
}
|
||||
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
|
||||
}
|
||||
for (gaps) |gap| {
|
||||
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
|
||||
|
||||
// Three holes below 4 GiB. This three is not a guess about hardware: a
|
||||
// `DeviceDescriptor` carries `maximum_device_resources` (8) resources, and the
|
||||
// bridge spends them on ECAM + bus range + these + the high aperture. That cap is
|
||||
// a wire struct in the kernel↔user ABI, so widening it is a separate change —
|
||||
// docs/bounds-track-plan.md, phase 4. Keeping *fewer* holes is fail-closed: it
|
||||
// refuses BARs, it never admits one.
|
||||
var holes: [3]AddressRange = undefined;
|
||||
_ = largestHolesBelow4G(boot_memory_regions, 1 << 20, &holes);
|
||||
for (holes) |hole| {
|
||||
if (hole.end > hole.base) _ = bridge.addResource(.memory, hole.base, hole.end - hole.base);
|
||||
}
|
||||
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user