library: the harness keeps the subscribers, and an id belongs to whoever opened it

Three services had each written the same thing and got it three different
ways: input polled the process list to notice a dead subscriber, and only
when someone else subscribed; the power service never noticed at all; the
device manager noticed drivers but not subscribers. The harness owns the
table now, driven by the events a protocol declares — it registers on the
reserved verb, frames each event once, posts to everyone interested without
waiting on any of them, and reclaims a slot when the kernel says its owner
died. Interest masks moved to the envelope, so a subscriber that wants only
mice asks the same way everywhere.

Two consequences the plan had not foreseen. The device manager now hears a
supervised child's death twice, once as its supervisor and once as a
subscriber, so restart backoff counted every crash twice and gave up after
half as many; it retires the id before counting. And the kernel's published
exit table had eight slots for what is now six subscriptions in a plain
boot, so it holds sixteen.

The other half is a hole the design named early and left standing: a
backend handed out a small integer and then honoured it from anyone. A
process that guessed a file's node id read another client's file; a display
layer had no owner at all, so any client could reconfigure or destroy any
layer; a USB device token was never checked against the client that opened
it. Each is now bound to the task that opened it, and a wrong owner gets
exactly what an unknown id gets — the refusal must not become the oracle
the identical answers elsewhere were designed to remove. Closing a file
changed with it: it used to succeed unconditionally, which would have told
a caller which ids existed.

Suite 111/111, with a new case in which one process holds a file and a
layer, hands both ids to a second process, and finds them untouched after
that process has tried everything with them.
This commit is contained in:
Daniel Samson
2026-08-01 09:05:26 +01:00
parent 2719b93530
commit 1b1c587c14
29 changed files with 1072 additions and 335 deletions
+11 -25
View File
@@ -13,7 +13,8 @@
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: its shape — a
//! synchronous call whose attached capability is the subscriber's endpoint — is exactly
//! what `envelope.operation_subscribe` means everywhere. The interest mask travels as the
//! packet's tail (`Subscribe`), because a reserved verb carries no typed request.
//! packet's tail (`envelope.Subscription`), because a reserved verb carries no typed
//! request; what this protocol supplies is the *meaning* of its bits — the device classes.
//! - **publish** is this protocol's one verb: a source sends one `InputEvent` and the
//! service answers at once, so publishing never blocks on a slow subscriber.
//! - **delivery** is an event push: the service `ipc_send`s each event to every interested
@@ -294,12 +295,6 @@ pub const InputEvent = extern struct {
// --- the contract -----------------------------------------------------------
/// The body of a `subscribe` — the envelope's reserved verb 2, whose shape (a call whose
/// capability is the subscriber's own endpoint) this protocol adopts wholesale. A reserved
/// verb has no typed request, so the mask travels as the packet's tail and `encodeSubscribe`
/// is how a client lays it down. Zero means every class.
pub const Subscribe = extern struct { device_mask: u32 = 0 };
pub const Protocol = envelope.Define(.{
.name = "input",
.version = 1,
@@ -334,23 +329,12 @@ pub fn eventOfDevice(device: u32) ?Event {
}
/// Frame a `subscribe` request: the reserved verb's header, then the interest mask. Null if
/// the buffer is too small. Spelled here rather than at each caller so the one place that
/// knows a reserved verb carries its body in the tail is the protocol module.
/// the buffer is too small. The mask itself is the envelope's `Subscription` — the interest
/// a reserved subscribe carries is universal, and the *meaning* of its bits (here: the
/// device classes above) is what each protocol supplies. Kept as a named helper because
/// `device_mask` is what an input caller calls it.
pub fn encodeSubscribe(device_mask: u32, buffer: []u8) ?[]u8 {
const total = envelope.prefix_size + @sizeOf(Subscribe);
if (buffer.len < total) return null;
const header = envelope.Header{ .operation = envelope.operation_subscribe };
const body = Subscribe{ .device_mask = device_mask };
@memcpy(buffer[0..envelope.prefix_size], std.mem.asBytes(&header));
@memcpy(buffer[envelope.prefix_size..][0..@sizeOf(Subscribe)], std.mem.asBytes(&body));
return buffer[0..total];
}
/// The interest mask out of a `subscribe` packet's tail, on the provider's side. A caller
/// that sent no mask at all means every class, which is what a zero mask means anyway.
pub fn decodeSubscribe(tail: []const u8) Subscribe {
if (tail.len < @sizeOf(Subscribe)) return .{};
return std.mem.bytesToValue(Subscribe, tail[0..@sizeOf(Subscribe)]);
return envelope.encodeSubscribe(device_mask, buffer);
}
test "an event of every class fits the push floor, header included" {
@@ -388,7 +372,9 @@ test "a subscribe carries its mask in the tail of the reserved verb" {
var buffer: [envelope.packet_maximum]u8 = undefined;
const packet = encodeSubscribe(device_mouse, &buffer).?;
try std.testing.expectEqual(envelope.operation_subscribe, envelope.headerOf(packet).?.operation);
try std.testing.expectEqual(device_mouse, decodeSubscribe(packet[envelope.prefix_size..]).device_mask);
// The provider side of this is the service harness's, which reads the same
// interest mask out of the tail for every protocol.
try std.testing.expectEqual(device_mouse, envelope.decodeSubscribe(packet[envelope.prefix_size..]).interest);
// A caller that sent nothing at all reads as the every-class mask.
try std.testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).device_mask);
try std.testing.expectEqual(@as(u32, 0), envelope.decodeSubscribe(&.{}).interest);
}