kernel: maximum_children_per_parent is gone

The second invented ceiling. It was written to stop a driver looping
device_register and exhausting a shared table — but there is no shared table
to exhaust any more, and each registrar already has its own allowance, so a
runaway costs only itself.

It never bounded a determined caller in the first place: 16 children per
parent, and nothing stopped it claiming more parents. What it reliably did
was refuse a real PCI bus with more than 16 functions, which is how an AMD
Ryzen booted with a working display, no USB and no storage.

The constant, its check, and the now-unused childCount all go. TooManyChildren
survives with one meaning instead of two: the caller is at its per-registrar
allowance.

This was unblocked from the moment D9 landed. The plan said so — "once the
quota exists the per-parent cap is redundant whether or not D6 has landed" —
in the same edit that left the step tagged "blocked on D6". Three iterations
were then spent re-reading that tag instead of the sentence beside it.

The containment test now asserts 64 children under one parent, four times
the old ceiling; restoring the cap fails it.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 21:12:20 +01:00
parent 3ae541214f
commit 7d8aa51234
3 changed files with 54 additions and 75 deletions
+1 -26
View File
@@ -53,22 +53,6 @@ const heap = @import("heap.zig");
/// found against registered)
const maximum_devices_per_registrar = 4096;
/// Cap on children a single parent may have. A zero-resource child (legal — a USB
/// device is addressed through its controller, not by MMIO) sidesteps the containment
/// check, so without a bound a process that claimed one device could loop
/// `device_register` and exhaust the whole table, permanently denying it to every other
/// driver. This bounds the blast radius of one claim; a real quota (and a
/// `device_release` to reclaim on exit) is future work — see docs/driver-model.md.
///
/// bound: children one claimed parent may register — in practice every PCI function on
/// the machine, since pci-bus registers them all under the one host bridge
/// decided-by: hardware
/// protects: the shared device table, against a driver looping device_register — but
/// it is a proxy for an authorisation the kernel does not perform, since any
/// process may claim any unclaimed device (docs/bounds-track-plan.md phase 2)
/// at-limit: refuse — ECHILDREN, distinct from a full table
/// observed-by: pci-bus logs the reason per refused function, and warns at end of scan
const maximum_children_per_parent = 16;
/// The table, grown on demand from the kernel heap. **There is no ceiling**: how many
/// devices a machine has is the machine's business, and no specification bounds it, so
@@ -368,7 +352,7 @@ pub const RegisterError = error{
NoSuchParent, // no device with that id
NotYourParent, // that device exists but this task has not claimed it
TooManyResources, // the descriptor declares more resources than one device may hold
TooManyChildren, // this parent is at maximum_children_per_parent
TooManyChildren, // the caller is at its per-registrar allowance
NotContained, // a child resource escapes its parent's window
};
@@ -459,14 +443,6 @@ fn existingChild(parent_id: u64, descriptor: *const device_abi.DeviceDescriptor)
return null;
}
/// Number of devices currently recorded with `parent_id` as their parent.
fn childCount(parent_id: u64) usize {
var n: usize = 0;
for (devices[0..count]) |d| {
if (d.parent == parent_id) n += 1;
}
return n;
}
/// Publish `descriptor` as a child of `parent_id`, on behalf of `owner`. Returns the new
/// device id. The child is left **unclaimed**, so another process (a class driver)
@@ -497,7 +473,6 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device
// below).
if (existingChild(parent_id, descriptor)) |existing_id| return existing_id;
if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
// The allowance is charged to whoever is registering, so a driver in a loop
// exhausts its own and every other driver carries on. There is no machine-wide
// ceiling any more: the table grows.
+18 -19
View File
@@ -4037,34 +4037,33 @@ fn containmentTest() void {
check("re-registering an identical child returns the same id", again != 0 and again == good);
check("re-registering grew nothing", devices_broker.enumerate(&buffer) == before + 1);
// Fill the parent to its child cap with distinct children (same window, different
// identity — the match is on identity, so each is a new device).
// **There is no per-parent cap.** `maximum_children_per_parent = 16` is gone: it
// was written to stop a driver looping `device_register` and exhausting a shared
// table, and there is no shared table to exhaust — the table grows, and each
// registrar has its own allowance, so a runaway costs only itself. The cap never
// bounded a determined caller anyway (16 per parent, but nothing stopped it
// claiming more parents); what it reliably did was refuse a real PCI bus with more
// than 16 functions, which is how an AMD Ryzen booted with no USB and no storage.
//
// 64 children under one parent — four times the old ceiling.
var filled: u32 = 0;
var capped = false;
var registered_children: u32 = 0;
while (filled < 64) : (filled += 1) {
var name: [4]u8 = .{ 'k', 0, 0, 0 };
name[1] = '0' + @as(u8, @intCast(filled / 10));
name[2] = '0' + @as(u8, @intCast(filled % 10));
var extra = childDescriptor(name[0..3], parent_window.start, 0x20);
_ = devices_broker.register(parent_id, me, &extra) catch |err| {
capped = err == error.TooManyChildren;
break;
};
if (devices_broker.register(parent_id, me, &extra)) |_| {
registered_children += 1;
} else |_| break;
}
check("the parent reaches its child cap (TooManyChildren)", capped);
check("one parent takes far more children than the old cap allowed", registered_children == 64);
// The regression this ordering exists for: **a re-registration consumes no slot,
// so a full parent must not refuse one.** A crashed bus driver is restarted by its
// supervisor and re-registers everything it rediscovers; when the cap was checked
// before the identity match, the restart was refused its own devices and the
// machine degraded a little more on every crash.
// Idempotency still holds, and still matters: a crashed bus driver is restarted and
// re-registers everything it rediscovers, which must return the ids it had before
// rather than duplicate them.
const readmitted = devices_broker.register(parent_id, me, &fits) catch 0;
check("a full parent still re-admits an identical child", readmitted != 0 and readmitted == good);
// ...and the cap is genuinely still in force for anything new.
var novel = childDescriptor("knew", parent_window.start, 0x20);
const still_capped = if (devices_broker.register(parent_id, me, &novel)) |_| false else |err| err == error.TooManyChildren;
check("a full parent still refuses a new child", still_capped);
check("re-registering an identical child still returns its id", readmitted != 0 and readmitted == good);
// The table itself has no ceiling: it grows. The old `maximum_devices = 64` was a
// guess about someone else's computer, and one driver's enumeration starved every