block: reclaim range slots on death, and give confinement one controller

Two defects the V2 boundary review confirmed:

1. The per-badge range table was never reclaimed. usb-storage receives exit
   notifications (via the subscriber watch), but onNotification handled only
   the timer and dropped child-exits, so a dead filesystem left its range
   slot used forever. The medium-removal lifecycle churns filesystems, so
   after maximum_ranges confine/die cycles define_range would return ENOSPC
   and no volume could be confined again until reboot. onNotification now
   frees the dead badge's slot, mirroring fat's open-node sweep.

2. define_range checked only that the CALLER was unconfined, never that it
   owned the target badge — so any unconfined opener could install a range
   for another live client and silently redirect its I/O. Confinement now has
   a single controller: the first unconfined party to define a range (the
   volume manager, which confines every filesystem before handing it a
   channel). Only the controller may thereafter; the slot releases on its
   death so a restarted manager re-takes it. This is the mechanism half; V3
   adds the grant half (only the volume manager gets an unconfined channel).

Neutral: block-range (the fixture is the sole definer -> controller),
fat-mount, usb-report all green. End-to-end exercise of both lands in V3/V4
(the VM+filesystem relationship and the remount churn).
This commit is contained in:
Daniel Samson
2026-08-09 17:58:47 +01:00
parent 89d4592777
commit bc67771bfd
+29 -2
View File
@@ -208,6 +208,14 @@ const maximum_ranges = 64;
const Range = struct { used: bool = false, badge: u32 = 0, base: u64 = 0, count: u64 = 0 }; const Range = struct { used: bool = false, badge: u32 = 0, base: u64 = 0, count: u64 = 0 };
var ranges = [_]Range{.{}} ** maximum_ranges; var ranges = [_]Range{.{}} ** maximum_ranges;
/// The one party allowed to confine others — the first unconfined caller to
/// define a range, which is the volume manager (it probes and confines every
/// filesystem before handing it a channel). Without this, any unconfined
/// opener could install a range for another live client's badge and silently
/// redirect its I/O. Released on the controller's death so a restarted volume
/// manager re-takes it.
var range_controller: ?u32 = null;
fn rangeFor(badge: u32) ?*Range { fn rangeFor(badge: u32) ?*Range {
for (&ranges) |*r| { for (&ranges) |*r| {
if (r.used and r.badge == badge) return r; if (r.used and r.badge == badge) return r;
@@ -264,7 +272,17 @@ fn onWrite(_: void, invocation: Invocation(block_protocol.Transfer), answer: Ans
/// Refused if the CALLER is itself confined — a filesystem cannot widen its own /// Refused if the CALLER is itself confined — a filesystem cannot widen its own
/// range or confine anyone; only an unconfined party (the volume manager) may. /// range or confine anyone; only an unconfined party (the volume manager) may.
fn onDefineRange(_: void, invocation: Invocation(block_protocol.DefineRange), _: Answer(void)) isize { fn onDefineRange(_: void, invocation: Invocation(block_protocol.DefineRange), _: Answer(void)) isize {
if (rangeFor(invocation.sender) != null) return -envelope.EPERM; const sender = invocation.sender;
// A confined caller may never confine — no self-widening, no escape.
if (rangeFor(sender) != null) return -envelope.EPERM;
// Confinement has a single controller (the volume manager). Whoever defines
// the first range takes it; only they may thereafter, so a second unconfined
// opener cannot install a range for a badge it does not own.
if (range_controller) |c| {
if (sender != c) return -envelope.EPERM;
} else {
range_controller = sender;
}
const request = invocation.request; const request = invocation.request;
const slot = rangeFor(request.badge) orelse free: { const slot = rangeFor(request.badge) orelse free: {
for (&ranges) |*r| { for (&ranges) |*r| {
@@ -336,8 +354,17 @@ fn onNotification(badge: u64) void {
if (got.isTimer()) { if (got.isTimer()) {
pollPresence(); pollPresence();
_ = time.timerOnce(service_endpoint, presence_poll_ms); _ = time.timerOnce(service_endpoint, presence_poll_ms);
return;
}
// A client died. The harness sweep (Serve.hooks) covers only the SUBSCRIBER
// table; the per-badge range table is ours to reclaim, or confine/die cycles
// (the medium-removal lifecycle) would exhaust it. A dead controller also
// releases confinement authority to its successor.
if (got.isChildExit()) {
const dead = got.childProcessId();
if (rangeFor(dead)) |r| r.* = .{};
if (range_controller == dead) range_controller = null;
} }
// Subscriber deaths are swept by the harness (Serve.hooks); nothing else here.
} }
pub fn main(init: process.Init) void { pub fn main(init: process.Init) void {