library: five protocols speak the envelope

The folded header stops being a rule in a document and becomes the layout
on the wire. Verbs number from sixteen, leaving describe, enumerate,
subscribe and unsubscribe reserved and answered the same way by every
provider — none of them writes a line to do it. What each protocol used to
carry in a field of its own now travels in the header: a vfs node and a
display layer are the packet's target, and a reply opens with a status the
envelope stamps rather than one each protocol spelled for itself.

Display gains the most. One forty-byte request had served eleven verbs, so
attach_scanout smuggled stride through x, refresh through y and format
through colour, and every coordinate crossed as a bitcast. Per-operation
structs end all three: the fields have their own names and their own signs,
and the tile payload grows to 224 bytes because the prefix shrank. Scanout
loses a message maximum of 64 it had no business declaring — it answers
calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding
that number at its harness.

Two changes are semantic rather than notational. A directory now ends at an
entry with no name, because the fixed part of a reply always travels and a
zero-length reply no longer exists to mean anything. And input joins the
service harness, the last loop in the tree that answered no ping and heard
no terminate; its subscriber table, its pruning and its fan-out are the
same code, and a shutdown now asks it to stop instead of killing it.

A new conformance case reads the registry's own listing and asks every
protocol it finds for its name, its version and its verb count, then offers
a verb nobody defines and requires -ENOSYS — the envelope's promise,
checked against providers rather than against itself. What it cannot reach
in that boot it names on the serial line instead of passing quietly.

Suite 110/110.
This commit is contained in:
Daniel Samson
2026-08-01 06:15:25 +01:00
parent b004b9c3eb
commit d2dfbcabf8
40 changed files with 1882 additions and 1008 deletions
+50 -30
View File
@@ -548,34 +548,43 @@ var heartbeat_running = false;
/// `pending_capability`. `arrived` is the capability the *request* carried, owned
/// by the turn — nothing here has to close it, only `bind` has to claim it.
fn serveRegistry(request_bytes: []const u8, reply: []u8, sender: u32, arrived: *Arrival) usize {
if (request_bytes.len < vfs_protocol.request_size)
return answer(reply, -envelope.EPROTO, 0, 0);
// The header is read field by field rather than reinterpreted whole: the
// operation is an enum on the wire and the bytes come from anyone at all, so
// a value outside it must be a refusal, never a decoded enum.
if (request_bytes.len < envelope.prefix_size)
return answer(reply, -envelope.EPROTO, 0);
// The header is read field by field rather than reinterpreted whole, and the
// verb is compared as a number rather than decoded into the generated
// `Operation`: the bytes come from anyone at all, so a value outside the enum
// must be a refusal, never an `@enumFromInt`. This is deliberately NOT
// `Protocol.Provider.dispatch` for the same reason — PID 1 reads a stranger's
// packet, and it reads it by hand.
const operation = std.mem.readInt(u32, request_bytes[0..4], .little);
const cursor = std.mem.readInt(u64, request_bytes[16..24], .little);
const declared = std.mem.readInt(u32, request_bytes[24..28], .little);
const payload_len = @min(@as(usize, declared), request_bytes.len - vfs_protocol.request_size);
const payload = request_bytes[vfs_protocol.request_size..][0..payload_len];
const body = request_bytes[envelope.prefix_size..];
if (operation == @intFromEnum(vfs_protocol.Operation.bind))
return answer(reply, onBind(sender, payload, arrived), 0, 0);
return answer(reply, onBind(sender, body, arrived), 0);
// Only `bind` claims a capability; one attached to anything else is closed by
// the turn's `defer` in the loop, along with the ones sent to a request that
// was too short to name a verb at all.
if (operation == @intFromEnum(vfs_protocol.Operation.open)) return onOpen(reply, sender, payload);
if (operation == @intFromEnum(vfs_protocol.Operation.readdir)) return onReaddir(reply, cursor);
if (operation == @intFromEnum(vfs_protocol.Operation.open)) {
// `open`'s fixed part is the flags word, which means nothing to a
// namespace; the name follows it as the packet's tail.
if (body.len < @sizeOf(vfs_protocol.Open)) return answer(reply, -envelope.EPROTO, 0);
return onOpen(reply, sender, body[@sizeOf(vfs_protocol.Open)..]);
}
if (operation == @intFromEnum(vfs_protocol.Operation.readdir)) {
if (body.len < @sizeOf(vfs_protocol.Readdir)) return answer(reply, -envelope.EPROTO, 0);
return onReaddir(reply, std.mem.readInt(u64, body[0..8], .little));
}
// Everything else a filesystem answers is meaningless here: `/protocol` holds
// contracts, not bytes.
return answer(reply, -envelope.ENOSYS, 0, 0);
return answer(reply, -envelope.ENOSYS, 0);
}
/// Lay down a vfs reply header (and say how many payload bytes follow it).
fn answer(reply: []u8, status: i32, node: u64, payload_len: usize) usize {
const header = vfs_protocol.Reply{ .status = status, .node = node, .len = @intCast(payload_len) };
@memcpy(reply[0..vfs_protocol.reply_size], std.mem.asBytes(&header));
return vfs_protocol.reply_size + payload_len;
/// Lay down the envelope's reply prefix (and say how many payload bytes the
/// caller has already written after it).
fn answer(reply: []u8, status: i32, payload_len: usize) usize {
const header = envelope.Status{ .status = status, .len = @intCast(payload_len) };
@memcpy(reply[0..envelope.prefix_size], std.mem.asBytes(&header));
return envelope.prefix_size + payload_len;
}
/// `bind(name, capability = the provider's endpoint)`. The capability is the
@@ -667,15 +676,19 @@ fn onBind(sender: u32, raw_name: []const u8, arrived: *Arrival) i32 {
/// other, a line in a world-readable log ring, or a serial write costing
/// milliseconds.)
fn onOpen(reply: []u8, sender: u32, raw_name: []const u8) usize {
const name = contractName(raw_name) orelse return answer(reply, -envelope.ENOENT, 0, 0);
const name = contractName(raw_name) orelse return answer(reply, -envelope.ENOENT, 0);
refreshProcessTable();
const identity = identify(sender);
const permitted = if (identity) |who| mayOpen(who, name) else false;
const binding = findBinding(name);
if (!permitted) return answer(reply, -envelope.ENOENT, 0, 0);
const found = binding orelse return answer(reply, -envelope.ENOENT, 0, 0);
if (!permitted) return answer(reply, -envelope.ENOENT, 0);
const found = binding orelse return answer(reply, -envelope.ENOENT, 0);
pending_capability = found.endpoint;
return answer(reply, 0, 0, 0);
// A contract node has no node id — the capability is the whole answer — but
// the protocol says an `open` reply carries one, so it carries a zero.
const opened = vfs_protocol.Opened{ .node = 0 };
@memcpy(reply[envelope.prefix_size..][0..@sizeOf(vfs_protocol.Opened)], std.mem.asBytes(&opened));
return answer(reply, 0, @sizeOf(vfs_protocol.Opened));
}
/// `readdir(cursor)` — the namespace, browsable. One entry per turn, as the vfs
@@ -690,18 +703,25 @@ fn onReaddir(reply: []u8, cursor: u64) usize {
continue;
}
const name = binding.nameSlice();
const entry = vfs_protocol.DirectoryEntry{
return writeEntry(reply, .{
.kind = @intFromEnum(vfs_protocol.NodeKind.protocol),
.name_len = @intCast(name.len),
.size = binding.task,
};
const total = vfs_protocol.directory_entry_size + name.len;
if (vfs_protocol.reply_size + total > reply.len) return answer(reply, -envelope.EPROTO, 0, 0);
@memcpy(reply[vfs_protocol.reply_size..][0..vfs_protocol.directory_entry_size], std.mem.asBytes(&entry));
@memcpy(reply[vfs_protocol.reply_size + vfs_protocol.directory_entry_size ..][0..name.len], name);
return answer(reply, 0, 0, total);
}, name);
}
return answer(reply, 0, 0, 0); // end of directory
// End of directory, which the envelope spells as an entry with no name: the
// reply's own length cannot say it any more, because the fixed reply part
// always travels.
return writeEntry(reply, .{}, &.{});
}
/// One `readdir` reply: the entry, then its name inline.
fn writeEntry(reply: []u8, entry: vfs_protocol.DirectoryEntry, name: []const u8) usize {
const total = vfs_protocol.directory_entry_size + name.len;
if (envelope.prefix_size + total > reply.len) return answer(reply, -envelope.EPROTO, 0);
@memcpy(reply[envelope.prefix_size..][0..vfs_protocol.directory_entry_size], std.mem.asBytes(&entry));
@memcpy(reply[envelope.prefix_size + vfs_protocol.directory_entry_size ..][0..name.len], name);
return answer(reply, 0, total);
}
pub fn main(startup: process.Init) void {