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
+70 -19
View File
@@ -19,6 +19,7 @@ const std = @import("std");
const channel = @import("channel");
const ipc = @import("ipc");
const input = @import("input-client");
const process = @import("process");
const Thread = @import("thread").Thread;
const service = @import("service");
const time = @import("time");
@@ -74,8 +75,22 @@ var background: u32 = 0;
/// small copies rather than one huge bounding box (see compositor.DamageList).
const maximum_layers = 16;
/// A layer belongs to the client that created it. `owner` is the kernel-stamped
/// badge of that task, and `service_owned` (0, an id no task wears) marks the
/// compositor's own layers — the cursor sprite and the self-check pair — which
/// this file creates by direct call rather than over the protocol.
///
/// Layer ids are slots in a sixteen-entry table: small, dense, and guessable, so
/// before this field any client could configure, draw into, or destroy any
/// other's layer — including the cursor. The check lives in the protocol handlers
/// (docs/os-development/protocol-namespace.md: handles validated against the
/// badge); the internal helpers stay unscoped precisely so the compositor can
/// still drive its own.
const service_owned: u32 = 0;
const Layer = struct {
used: bool = false,
owner: u32 = service_owned,
x: i32 = 0,
y: i32 = 0,
z: u32 = 0,
@@ -189,7 +204,8 @@ fn layerAt(id: u32) ?*Layer {
return &layers[id];
}
fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
/// Create a layer for `owner` — `service_owned` for the compositor's own.
fn createLayer(owner: u32, x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
if (w == 0 or h == 0) return null;
const slot = freeLayer() orelse return null;
const len = @as(usize, w) * h * 4;
@@ -197,6 +213,7 @@ fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
if (memory.mmapFailed(base)) return null;
layers[slot] = .{
.used = true,
.owner = owner,
.x = x,
.y = y,
.z = z,
@@ -420,8 +437,8 @@ fn selfCheck() void {
const format = backend.info().format;
const red = display_protocol.pack(format, 0xC0, 0x20, 0x20);
const green = display_protocol.pack(format, 0x20, 0xC0, 0x20);
const bottom = createLayer(100, 100, 80, 80, 0, true) orelse return fail_check("create");
const top = createLayer(140, 140, 80, 80, 1, true) orelse return fail_check("create");
const bottom = createLayer(service_owned, 100, 100, 80, 80, 0, true) orelse return fail_check("create");
const top = createLayer(service_owned, 140, 140, 80, 80, 1, true) orelse return fail_check("create");
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
present();
@@ -586,7 +603,7 @@ fn startCursorTracking() void {
const mode = backend.info();
cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2);
cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2);
const id = createLayer(cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
const id = createLayer(service_owned, cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
_ = logging.write("display: could not create cursor layer\n");
return;
};
@@ -603,6 +620,9 @@ fn startCursorTracking() void {
fn initialise(endpoint: ipc.Handle) bool {
service_endpoint = endpoint;
// Layers are per-client state, so the compositor needs deaths: a client that
// crashes leaves its surfaces on screen and its slots spent otherwise.
_ = process.subscribeExits(endpoint);
// Pick the scanout backend (GOP today). It logs the reason on failure.
backend = backend_mod.select() orelse return false;
@@ -635,9 +655,35 @@ fn initialise(endpoint: ipc.Handle) bool {
// id a u32: a value that does not fit is not a layer of ours, and `layerAt`
// refuses it the same way an out-of-range one is refused.
fn targetLayer(target: u64) ?u32 {
if (target > std.math.maxInt(u32)) return null;
return @intCast(target);
/// The layer a packet addresses, **for the task that sent it**: null unless the
/// target names a used slot this sender created. A layer that is somebody else's
/// is refused exactly as one that never existed, so a client cannot use the
/// refusal to learn which ids are live (P3's refusal-equals-absence, applied to
/// ids rather than names).
fn targetLayer(target: u64, sender: u32) ?u32 {
if (target > std.math.maxInt(u32)) return null; // a layer id is a u32
const id: u32 = @intCast(target);
const layer = layerAt(id) orelse return null;
if (layer.owner != sender) return null;
return id;
}
/// Destroy every layer a dead client left behind — its surface is pages nobody
/// will ever draw into again, and its slot is one of sixteen. The published
/// exit events are the notice, the same sweep idiom the FAT server uses for open
/// files and the harness uses for subscribers.
fn releaseLayersOf(dead: u32) void {
var released: u32 = 0;
for (&layers, 0..) |*layer, id| {
if (layer.used and layer.owner == dead) {
_ = destroyLayer(@intCast(id));
released += 1;
}
}
if (released != 0) {
std.log.info("released {d} layer(s) for dead client {d}", .{ released, dead });
schedulePresent(); // the screen still shows what they painted
}
}
fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) isize {
@@ -648,37 +694,37 @@ fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) i
fn onCreateLayer(_: void, invocation: Invocation(display_protocol.CreateLayer), answer: Answer(display_protocol.Created)) isize {
const request = invocation.request;
const slot = createLayer(request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused;
const slot = createLayer(invocation.sender, request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused;
answer.set(.{ .layer = slot });
return 0;
}
fn onConfigureLayer(_: void, invocation: Invocation(display_protocol.ConfigureLayer), _: Answer(void)) isize {
const id = targetLayer(invocation.target) orelse return refused;
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
const request = invocation.request;
return if (configureLayer(id, request.x, request.y, request.z, request.visible != 0)) 0 else refused;
}
fn onDestroyLayer(_: void, invocation: Invocation(void), _: Answer(void)) isize {
const id = targetLayer(invocation.target) orelse return refused;
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
return if (destroyLayer(id)) 0 else refused;
}
fn onFillRect(_: void, invocation: Invocation(display_protocol.FillRect), _: Answer(void)) isize {
const id = targetLayer(invocation.target) orelse return refused;
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
const request = invocation.request;
const local = Rect.init(request.x, request.y, @intCast(request.width), @intCast(request.height));
return if (fillLayer(id, local, request.colour)) 0 else refused;
}
fn onBlitTile(_: void, invocation: Invocation(display_protocol.BlitTile), _: Answer(void)) isize {
const id = targetLayer(invocation.target) orelse return refused;
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
const request = invocation.request;
return if (blitLayer(id, request.x, request.y, request.width, request.height, invocation.tail)) 0 else refused;
}
fn onDamage(_: void, invocation: Invocation(display_protocol.Damage), _: Answer(void)) isize {
const id = targetLayer(invocation.target) orelse return refused;
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
const l = layerAt(id) orelse return refused;
const request = invocation.request;
const screen = Rect{ .x = l.x + request.x, .y = l.y + request.y, .w = @intCast(request.width), .h = @intCast(request.height) };
@@ -733,13 +779,18 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arriva
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
}
/// Two notification sources reach the compositor, and one coalesced badge can carry
/// both, so each bit is handled independently. A **message-notification** is a poke from
/// the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`): fold the
/// newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the frame
/// clock — or the deferred first native present after `attach_scanout` — either way,
/// present the accumulated damage.
/// Three notification sources reach the compositor, and one coalesced badge can carry
/// more than one, so each bit is handled independently. A **message-notification** is a
/// poke from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
/// fold the newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the
/// frame clock — or the deferred first native present after `attach_scanout` — either way,
/// present the accumulated damage. A **published exit** (`notify_exit_bit`) is a client
/// gone: release the layers it left.
fn onNotification(badge: u64) void {
if (badge & ipc.notify_exit_bit != 0) {
releaseLayersOf(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
return;
}
if (badge & ipc.notify_message_bit != 0) renderCursor();
if (badge & ipc.notify_timer_bit != 0) frameTick();
}