diff --git a/system/drivers/usb-storage/usb-storage.zig b/system/drivers/usb-storage/usb-storage.zig index 1351378..84956ea 100644 --- a/system/drivers/usb-storage/usb-storage.zig +++ b/system/drivers/usb-storage/usb-storage.zig @@ -208,6 +208,14 @@ const maximum_ranges = 64; const Range = struct { used: bool = false, badge: u32 = 0, base: u64 = 0, count: u64 = 0 }; 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 { for (&ranges) |*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 /// range or confine anyone; only an unconfined party (the volume manager) may. 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 slot = rangeFor(request.badge) orelse free: { for (&ranges) |*r| { @@ -336,8 +354,17 @@ fn onNotification(badge: u64) void { if (got.isTimer()) { pollPresence(); _ = 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 {