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:
@@ -95,9 +95,13 @@ const Answer = struct {
|
||||
/// Whether a capability rode the reply. The one field that actually matters
|
||||
/// to a client: the capability IS the channel.
|
||||
capability: bool = false,
|
||||
/// The reply header, decoded — compared field by field as well as byte for
|
||||
/// byte, so a failure says *which* field diverged.
|
||||
reply: vfs_protocol.Reply = .{ .status = 0, .node = 0, .len = 0 },
|
||||
/// The reply's envelope `Status`, decoded — compared field by field as well
|
||||
/// as byte for byte, so a failure says *which* field diverged.
|
||||
status: envelope.Status = .{ .status = 0, .len = 0 },
|
||||
/// The node id the reply carried, or null when it carried no reply body at
|
||||
/// all. A refusal has none; the open of a contract carries a zero, because
|
||||
/// the capability is the whole answer.
|
||||
node: ?u64 = null,
|
||||
|
||||
fn bytes(self: *const Answer) []const u8 {
|
||||
return self.packet[0..self.length];
|
||||
@@ -129,33 +133,30 @@ fn registryEndpoint() ?ipc.Handle {
|
||||
const resolve_attempts: u32 = 200;
|
||||
const resolve_retry_ms: u64 = 20;
|
||||
|
||||
/// One vfs-protocol request at the registry: the fixed header, then the contract
|
||||
/// name inline. Names go bare (`input`, not `/input`) — the registrar normalises
|
||||
/// both, and bare is what `bind` sends.
|
||||
fn transact(registry: ipc.Handle, operation: vfs_protocol.Operation, name: []const u8, cursor: u64) ?Answer {
|
||||
var request: [vfs_protocol.message_maximum]u8 = undefined;
|
||||
if (vfs_protocol.request_size + name.len > request.len) return null;
|
||||
const header = vfs_protocol.Request{
|
||||
.operation = operation,
|
||||
.node = 0,
|
||||
.offset = cursor,
|
||||
.len = @intCast(name.len),
|
||||
.flags = 0,
|
||||
};
|
||||
@memcpy(request[0..vfs_protocol.request_size], std.mem.asBytes(&header));
|
||||
@memcpy(request[vfs_protocol.request_size..][0..name.len], name);
|
||||
/// One vfs-protocol request at the registry: the folded header, the verb's own
|
||||
/// fixed part, then the contract name as the packet's tail. Names go bare
|
||||
/// (`input`, not `/input`) — the registrar normalises both, and bare is what
|
||||
/// `bind` sends.
|
||||
fn transact(
|
||||
registry: ipc.Handle,
|
||||
comptime operation: vfs_protocol.Operation,
|
||||
request: vfs_protocol.Protocol.RequestOf(operation),
|
||||
name: []const u8,
|
||||
) ?Answer {
|
||||
var packet: [vfs_protocol.message_maximum]u8 = undefined;
|
||||
const framed = vfs_protocol.Protocol.encodeRequest(operation, 0, request, name, &packet) orelse return null;
|
||||
|
||||
var answer: Answer = .{};
|
||||
const got = ipc.callCap(
|
||||
registry,
|
||||
request[0 .. vfs_protocol.request_size + name.len],
|
||||
&answer.packet,
|
||||
null,
|
||||
) catch return null;
|
||||
if (got.len < vfs_protocol.reply_size) return null;
|
||||
const got = ipc.callCap(registry, framed, &answer.packet, null) catch return null;
|
||||
answer.length = got.len;
|
||||
answer.capability = got.cap != null;
|
||||
answer.reply = std.mem.bytesToValue(vfs_protocol.Reply, answer.packet[0..vfs_protocol.reply_size]);
|
||||
answer.status = envelope.statusOf(answer.bytes()) orelse return null;
|
||||
answer.node = if (operation == .open) blk: {
|
||||
const opened = vfs_protocol.Protocol.decodeReply(.open, answer.bytes()) orelse break :blk null;
|
||||
// A short reply decodes as garbage rather than absence, so the promised
|
||||
// length is what says whether a body is there at all.
|
||||
break :blk if (answer.status.len < @sizeOf(vfs_protocol.Opened)) null else opened.node;
|
||||
} else null;
|
||||
// A capability we did not ask to keep is a handle slot spent; the assertions
|
||||
// below only care that one arrived.
|
||||
if (got.cap) |handle| _ = ipc.close(handle);
|
||||
@@ -165,7 +166,7 @@ fn transact(registry: ipc.Handle, operation: vfs_protocol.Operation, name: []con
|
||||
/// `open(name)`, kept whole. Null only if the registry could not be reached at
|
||||
/// all — a registrar that answered has decided, and its decision is the subject.
|
||||
fn openContract(registry: ipc.Handle, name: []const u8) Answer {
|
||||
return transact(registry, .open, name, 0) orelse fail("the registry stopped answering");
|
||||
return transact(registry, .open, .{ .flags = 0 }, name) orelse fail("the registry stopped answering");
|
||||
}
|
||||
|
||||
/// Whether `/protocol` currently lists `name`. The namespace is browsable on
|
||||
@@ -177,12 +178,11 @@ fn openContract(registry: ipc.Handle, name: []const u8) Answer {
|
||||
fn listed(registry: ipc.Handle, name: []const u8) bool {
|
||||
var cursor: u64 = 0;
|
||||
while (cursor < 64) : (cursor += 1) {
|
||||
const answer = transact(registry, .readdir, "", cursor) orelse return false;
|
||||
if (answer.reply.status != 0 or answer.reply.len == 0) return false; // end of directory
|
||||
const payload = answer.packet[vfs_protocol.reply_size..answer.length];
|
||||
if (payload.len < vfs_protocol.directory_entry_size) return false;
|
||||
const entry = std.mem.bytesToValue(vfs_protocol.DirectoryEntry, payload[0..vfs_protocol.directory_entry_size]);
|
||||
const text = payload[vfs_protocol.directory_entry_size..];
|
||||
const answer = transact(registry, .readdir, .{ .cursor = cursor }, "") orelse return false;
|
||||
if (answer.status.status != 0) return false;
|
||||
const entry = vfs_protocol.Protocol.decodeReply(.readdir, answer.bytes()) orelse return false;
|
||||
if (entry.name_len == 0) return false; // end of directory
|
||||
const text = vfs_protocol.Protocol.replyTail(.readdir, answer.bytes());
|
||||
const length = @min(@as(usize, entry.name_len), text.len);
|
||||
if (std.mem.eql(u8, text[0..length], name)) return true;
|
||||
}
|
||||
@@ -207,14 +207,24 @@ fn awaitListed(registry: ipc.Handle, name: []const u8) void {
|
||||
/// Every caller-visible field of two answers, compared. `step` names the pair so
|
||||
/// a failure says which comparison broke and in which field.
|
||||
fn expectIdentical(step: []const u8, refused: Answer, absent: Answer) void {
|
||||
if (refused.reply.status != absent.reply.status) fail(step); // the errno
|
||||
if (refused.reply.node != absent.reply.node) fail(step); // the node id an open would return
|
||||
if (refused.reply.len != absent.reply.len) fail(step); // payload bytes promised
|
||||
if (refused.status.status != absent.status.status) fail(step); // the errno
|
||||
if (!nodesMatch(refused.node, absent.node)) fail(step); // the node id an open would return
|
||||
if (refused.status.len != absent.status.len) fail(step); // payload bytes promised
|
||||
if (refused.length != absent.length) fail(step); // reply packet length
|
||||
if (refused.capability != absent.capability) fail(step); // the channel itself
|
||||
if (!std.mem.eql(u8, refused.bytes(), absent.bytes())) fail(step); // and every byte of it
|
||||
}
|
||||
|
||||
/// Two node ids agree when both are absent or both are the same value. A refusal
|
||||
/// carries none at all now — the envelope sends a bare `Status` — so "no node"
|
||||
/// is itself one of the observations that has to match.
|
||||
fn nodesMatch(one: ?u64, other: ?u64) bool {
|
||||
if (one) |a| {
|
||||
return if (other) |b| a == b else false;
|
||||
}
|
||||
return other == null;
|
||||
}
|
||||
|
||||
fn run() void {
|
||||
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
||||
|
||||
@@ -232,14 +242,14 @@ fn run() void {
|
||||
// 1. The control. A granted, bound contract opens: success, and the
|
||||
// capability that IS the channel.
|
||||
const allowed = openContract(registry, granted_contract);
|
||||
if (allowed.reply.status != 0) fail("a granted open was refused");
|
||||
if (allowed.status.status != 0) fail("a granted open was refused");
|
||||
if (!allowed.capability) fail("a granted open carried no channel");
|
||||
_ = logging.write("protocol-denied: granted open succeeded\n");
|
||||
|
||||
// 2. The refusal. `input` is bound — the listing above proved it — and no
|
||||
// manifest row names this binary against it.
|
||||
const refused = openContract(registry, forbidden_contract);
|
||||
if (refused.reply.status != -envelope.ENOENT) fail("an ungranted open did not answer -ENOENT");
|
||||
if (refused.status.status != -envelope.ENOENT) fail("an ungranted open did not answer -ENOENT");
|
||||
if (refused.capability) fail("an ungranted open carried a channel");
|
||||
_ = logging.write("protocol-denied: ungranted open refused as absent\n");
|
||||
|
||||
@@ -259,7 +269,7 @@ fn run() void {
|
||||
// what it should, so what steps 2-4 saw was policy and not a registry
|
||||
// that had wedged.
|
||||
const again = openContract(registry, granted_contract);
|
||||
if (again.reply.status != 0 or !again.capability) fail("the granted contract stopped opening");
|
||||
if (again.status.status != 0 or !again.capability) fail("the granted contract stopped opening");
|
||||
_ = logging.write("protocol-denied: ok\n");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user