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
+32 -70
View File
@@ -60,15 +60,14 @@ const pwrbtn_bit: u16 = 1 << 8;
const sci_en_bit: u32 = 1 << 0;
const slp_en: u32 = 1 << 13;
// The `.power` subscribers: endpoints handed over as capabilities, each
// receiving events as buffered messages. Dropped on a failed send. The
// subscriber's task id is kept too — a shutdown request is honored only from a
// subscriber (init subscribes; a stray process does not), the soft gate that
// stands in for "only the system supervisor may power off" without hardcoding
// a pid the kernel's idle tasks would have taken.
const maximum_subscribers = 8;
var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers;
var subscriber_tasks: [maximum_subscribers]u32 = .{0} ** maximum_subscribers;
/// The `.power` subscribers, kept by the service harness (P4c): endpoints handed
/// over as capabilities, each receiving events as buffered messages, each swept
/// when its task dies. The table remembers which task subscribed, which is what
/// the shutdown gate below asks — a shutdown request is honored only from a
/// subscriber (init subscribes; a stray process does not), the soft gate that
/// stands in for "only the system supervisor may power off" without hardcoding a
/// pid the kernel's idle tasks would have taken.
const Subscriptions = service.Subscribers(power_protocol.Protocol, void);
// Pass-1 registration record (see main): what pass 2 reports.
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
@@ -201,6 +200,7 @@ pub fn main(init: process.Init) void {
.init = onInit,
.on_message = onMessage,
.on_notification = onNotification,
.subscribers = Subscriptions.hooks,
});
}
@@ -407,13 +407,13 @@ fn publishNotify(node: *aml.Node, code: u64) void {
const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid };
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) {
publish(.battery, notice);
Subscriptions.publish(.battery, 0, notice);
} else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) {
publish(.ac, notice);
Subscriptions.publish(.ac, 0, notice);
} else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) {
publish(.lid, notice);
Subscriptions.publish(.lid, 0, notice);
} else {
publish(.notify, notice);
Subscriptions.publish(.notify, 0, notice);
}
}
@@ -425,28 +425,10 @@ fn writeHex2(out: []u8, n: u32) void {
}
fn publishButton() void {
publish(.power_button, .{});
}
/// Push one event to every subscriber. The kind is the packet's operation, so
/// this is framed once, outside the loop — every subscriber gets identical
/// bytes. A subscriber whose endpoint stops accepting (it died) is dropped on
/// the failed send, so a dead one can never stall the rest.
fn publish(comptime kind: power_protocol.Event, notice: power_protocol.Notice) void {
var packet: [envelope.post_maximum]u8 = undefined;
const framed = power_protocol.Protocol.encodeEvent(kind, 0, notice, &packet) orelse return;
for (&subscribers) |*slot| {
if (slot.*) |handle| {
if (!ipc.send(handle, framed)) slot.* = null;
}
}
}
fn isSubscriber(task: u32) bool {
for (&subscribers, 0..) |*slot, si| {
if (slot.* != null and subscriber_tasks[si] == task) return true;
}
return false;
// The kind is the packet's operation, so the harness frames it once and pushes
// the same bytes to every subscriber. There is no class here: a power event
// goes to everyone who asked for power events.
Subscriptions.publish(.power_button, 0, .{});
}
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
@@ -468,57 +450,37 @@ fn enterS5() void {
// --- harness callbacks --------------------------------------------------------
fn onNotification(badge: u64) void {
// The only notification the service binds is the SCI (an IRQ badge).
_ = badge;
// Two kinds of notification reach this loop now. The SCI is the one this
// service binds; the published process exits are the harness's, which it has
// already used to sweep the subscriber table before calling here. Everything
// that is not a bare IRQ badge must therefore be ignored — treating a death
// as an interrupt would clear PM1 status the firmware never set.
if (badge & (ipc.notify_exit_bit | ipc.notify_timer_bit | ipc.notify_message_bit | ipc.notify_signal_bit) != 0) return;
onSci();
}
/// The generated power dispatch. One provider per system, so the handler context
/// is empty and the subscriber table stays in this file's globals.
const Serve = power_protocol.Protocol.Provider(void);
const Invocation = envelope.Invocation;
const Answer = envelope.Answer;
/// Set by `onSubscribe` when the subscriber table has taken the capability the
/// call carried, and read by `onMessage`, where the turn's `Arrival` lives.
var capability_claimed = false;
/// The power contract: the reserved `subscribe` (the subscriber's endpoint as
/// the call's capability) and `shutdown` (subscribers only). Device discovery
/// uses a different endpoint — the device manager's — so nothing here handles a
/// tree report.
/// the call's capability, answered by the harness) and `shutdown` (subscribers
/// only). Device discovery uses a different endpoint — the device manager's — so
/// nothing here handles a tree report.
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
capability_claimed = false;
const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
if (capability_claimed) _ = arrived.take();
return written;
return Subscriptions.dispatch({}, handlers, message, sender, arrived, reply);
}
const handlers = Serve.Handlers{ .shutdown = onShutdown, .subscribe = onSubscribe };
/// The subscriber's endpoint is claimed only when a slot takes it; a full table
/// refuses and the turn closes what arrived.
fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize {
const endpoint = invocation.capability orelse return -envelope.EPROTO;
for (&subscribers, 0..) |*slot, index| {
if (slot.* == null) {
slot.* = endpoint;
subscriber_tasks[index] = invocation.sender;
capability_claimed = true;
return 0;
}
}
return -envelope.ENOSPC;
}
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
const handlers = Subscriptions.Handlers{ .shutdown = onShutdown };
/// Honored only from a power subscriber — init, which has already run the stop
/// sequence over everything else. The power service is mechanism (write S5);
/// deciding *when* to shut down and stopping the rest of the system first is
/// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing
/// in the packet can claim to be init.
/// in the packet can claim to be init. The subscriber table moved into the
/// harness; the question it answers has not changed.
fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize {
if (!isSubscriber(invocation.sender)) return -envelope.EPERM;
if (!Subscriptions.has(invocation.sender)) return -envelope.EPERM;
enterS5();
return 0;
}