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:
+35
-30
@@ -48,16 +48,17 @@ that cannot safely run in user space.**
|
||||
| D10 | Every driver hellos, on its own merits (liveness, one class of driver) | not started — optional, independent |
|
||||
| D6 | `device_claim` refuses a device the caller was not handed; the hole is closed | not started |
|
||||
| D7 | Zero-resource devices stop being kernel objects — inventory moves to the manager | **blocked** — nothing else mints their ids; see question 8 |
|
||||
| D8 | **`maximum_children_per_parent` deleted** — the authorisation it stood in for exists | **blocked on D6**, and now ordered after D9 |
|
||||
| D8 | **`maximum_children_per_parent` deleted** | **done** — it was unblocked from the moment D9 landed; I kept reading my own stale label |
|
||||
| D9 | The device table becomes dynamic; **`maximum_devices` deleted**; per-holder quota declared | **done** — one of the two invented numbers is gone |
|
||||
|
||||
**Run 2 stops, blocked.** Landed: D0, D1, D2, D4, D9, and D5 for three of five
|
||||
claimants (`usb-xhci-bus`, `pci-bus`, `virtio-gpu`). `maximum_devices` no longer exists,
|
||||
delegation is atomic with the spawn, and the suite is 118/118.
|
||||
**Run 2 stops, blocked on one question.** Landed: D0, D1, D2, D4, D8, D9, and D5 for
|
||||
three of five claimants (`usb-xhci-bus`, `pci-bus`, `virtio-gpu`). Suite 118/118.
|
||||
|
||||
Blocked: **D6 and D8 on question 9** (`ps2-bus` needs two devices and ignores its
|
||||
assignment; discovery needs a node nobody assigns), and **D7 on question 8**. So
|
||||
`maximum_children_per_parent` — the second invented number — is one answer away.
|
||||
**Both invented ceilings are gone.** `maximum_devices` and
|
||||
`maximum_children_per_parent` no longer exist, and delegation is atomic with the spawn.
|
||||
|
||||
What remains is not a number: **D6 closes the claiming hole** and is blocked on
|
||||
question 9, and D7 moves the inventory and is blocked on question 8.
|
||||
|
||||
Ordering is load-bearing. D1–D2 built and proved the mechanism with nothing depending on
|
||||
it. D4–D5 move each claimant across one at a time, so the suite stays green throughout
|
||||
@@ -90,34 +91,38 @@ They land together, with the manager claiming only for drivers in an explicit
|
||||
anything in D4 — recorded rather than dismissed, because D4 moved the `hello` earlier
|
||||
and so did shift boot timing. Watch it across the remaining steps.
|
||||
|
||||
### Open question 9 — two drivers need a device nobody assigned them
|
||||
### Open question 9 — danos has two device-acquisition patterns; D6 fits only one
|
||||
|
||||
D0 made delivery atomic and D5 converted `virtio-gpu` with it. The last two do not fit,
|
||||
for the same underlying reason: **they need a device the manager never assigned.**
|
||||
Earlier versions of this question listed symptoms — "`ps2-bus` ignores its `argv[1]`",
|
||||
"discovery has no assignment" — which hid that they are the same fact.
|
||||
|
||||
- **`ps2-bus` ignores its `argv[1]` entirely.** It finds the controller by walking the
|
||||
table for `PNP0303`, and then claims a *second* device — the `PNP0F13` mouse node —
|
||||
which it also finds itself. So it holds two devices and was assigned at most one, and
|
||||
`system_spawn` carries one.
|
||||
- **discovery** is spawned `addDriver("discovery", no_device, false)` and claims the
|
||||
`acpi-tables` node it locates itself, because it is what produces the device tree;
|
||||
there is nothing to assign at that point.
|
||||
| Pattern | Who | How it gets its device |
|
||||
|---|---|---|
|
||||
| Per-device driver | `pci-bus`, `usb-xhci-bus`, `virtio-gpu`, `usb-hid`, `usb-storage` | assigned one id, one instance per device — **all converted** |
|
||||
| Singleton that finds its own | `ps2-bus`, discovery | spawned once with `no_device`, walks the table itself |
|
||||
|
||||
Three shapes of answer, none written down:
|
||||
The second is deliberate, not an oversight. `onChildAdded` says so:
|
||||
|
||||
1. **The manager assigns every device a driver needs.** For `ps2-bus` it would have to
|
||||
understand that one driver serves both `PNP0303` and `PNP0F13` — `devices.csv` maps
|
||||
both to it already, so the manager may simply be spawning two instances today where
|
||||
the driver expects one. Worth checking before designing.
|
||||
2. **A driver asks for a device over the protocol** — a `request(device)` verb the
|
||||
manager answers by transferring. General, and it reintroduces a window, though only
|
||||
for a device the driver asks for after it is running.
|
||||
3. **The bootstrap is exempt**: discovery keeps claiming, and D6 permits a claim of a
|
||||
device nobody holds *only* for the discovery role. Narrow and honest, but it is an
|
||||
exception in the exact place an exception is most expensive.
|
||||
> An hid-matched driver (ps2-bus) is a singleton that finds its own devices once
|
||||
> spawned — spawn it once, no device assignment.
|
||||
|
||||
**D6 stays blocked** until this is answered — closing `device_claim` with these two
|
||||
still depending on it would stop the machine booting.
|
||||
`device_claim` is the mechanism that makes that pattern work. **D6 removes it and puts
|
||||
nothing in its place**, which is the whole of the blockage.
|
||||
|
||||
The manager is not ignorant: it matched *both* `PNP0303` and `PNP0F13` to `ps2-bus` and
|
||||
chose not to assign either, so it already knows which devices a singleton wants. An
|
||||
answer is therefore in reach — hand a singleton each matching device as it matches. The
|
||||
cost: only the first can ride the spawn, so later ones arrive while the driver is
|
||||
running, and `ps2-bus` enumerates once at startup and would have to tolerate that.
|
||||
|
||||
**The decision: do singletons stop being singletons — one instance per device, like
|
||||
everything else — or does the system keep a second acquisition path for them?** The
|
||||
first is uniform and costs a rewrite of `ps2-bus`'s startup. The second keeps a
|
||||
mechanism whose only remaining users are two drivers, and every exemption in an
|
||||
authority model is somewhere the model does not hold.
|
||||
|
||||
Nothing else blocks D6. Both invented ceilings are already gone, so this is about
|
||||
closing the claiming hole, not about a number.
|
||||
|
||||
### Settled 2026-08-08: the grant rides `system_spawn` (D0)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user