A protocol is reached by name now, not by a compile-time integer. Init is PID 1 and already knows which binary it started, so init serves /protocol as a vfs backend: bind claims a contract with the provider's endpoint attached, open answers with that endpoint as the reply's capability, and readdir lists what is bound with the task and binary behind it. The kernel reserves the prefix — nothing may mount over it, under it, or unmount it — and ServiceId, ipc_register and ipc_lookup are gone, their syscall numbers left vacant. A bind is authorized by who the caller *is*: the kernel-stamped binary together with the supervising task's identity, matched against /system/configuration/protocol.csv. Identity, not spelling — spawn is ungated, so an attacker can run any bundled binary, and a name-only rule would have let it launder grants through an init of its own making. A name a live process holds is refused to everyone else; a dead one's is released. Three review rounds against a hostile ring-3 process found what 108 green tests could not, because the suite contains no attacker. Publishing init's supervision endpoint as the registry put PID 1's mailbox in every process's hands, where two forged bytes reached the shutdown path: privileged traffic is now believed only from the task that holds the contract it speaks for. A capability arriving on a request outlived every path that ignored it, one handle per call until the table was full — in init, and in the harness ten services share — so the arriving capability is owned by the turn and released unless a handler says otherwise. And the kernel let anyone holding an endpoint handle aim signals, timers, exit notices and interrupts at it: binding now requires having created it. Suite 108/108. The new protocol-registry case asserts eleven properties, each one an attack that must fail.
925 lines
43 KiB
Zig
925 lines
43 KiB
Zig
//! The envelope — the fixed prefix every danos packet begins with, and the
|
|
//! comptime `Define` that builds a protocol out of it. Layer L2 of
|
|
//! [communication.md](../../../docs/os-development/communication.md); the
|
|
//! authoritative description is
|
|
//! [protocol-namespace.md](../../../docs/os-development/protocol-namespace.md).
|
|
//!
|
|
//! This is the one module in the protocol domain that is not itself a protocol:
|
|
//! it is the shape every protocol is expressed in. A protocol module hands
|
|
//! `Define` its verbs and events and gets back numbered operations (never
|
|
//! colliding with the reserved range), typed encode/decode helpers, a
|
|
//! provider-side dispatch table that answers `describe` on its own — and, the
|
|
//! point of the exercise, compile-time proof that none of its packets can
|
|
//! exceed the transport floor. Errors that used to surface as runtime
|
|
//! truncation are compile errors, and "packets never fragment" is enforced at
|
|
//! the source rather than by review.
|
|
//!
|
|
//! **The prefix is folded, never stacked.** A `request`, `reply`, or `payload`
|
|
//! type names the bytes that follow the prefix — never the whole packet. The
|
|
//! verb and the object being addressed live in the prefix, so a protocol type
|
|
//! carries neither an `operation` field of its own nor a nested `Header`;
|
|
//! `rejectStacking` refuses one at compile time, and every size check adds
|
|
//! `prefix_size` exactly once.
|
|
|
|
const std = @import("std");
|
|
|
|
// --- the prefix -------------------------------------------------------------
|
|
|
|
/// Every packet a danos protocol transmits begins with this header — requests
|
|
/// on the synchronous call path, event packets on the asynchronous push path.
|
|
/// A reply spends the same 16 bytes on `Status` instead.
|
|
pub const Header = extern struct {
|
|
/// The verb. Values below `first_protocol_operation` are the reserved
|
|
/// universal verbs, which mean the same thing in every protocol.
|
|
operation: u32,
|
|
_padding: u32 = 0,
|
|
/// **Object** addressing within the peer, never party addressing: which of
|
|
/// the peer's objects this packet operates on — a volume, a layer, a node,
|
|
/// a device. `0` addresses the provider itself, and a protocol with no
|
|
/// objects never uses the field. *Which* party is at the other end was
|
|
/// decided once, when the channel was opened, and *who sent this* is the
|
|
/// kernel-stamped badge; neither is ever written here, which is what keeps
|
|
/// the source unforgeable.
|
|
target: u64 = 0,
|
|
};
|
|
|
|
/// Every reply begins with this. `len` counts the bytes that follow: the
|
|
/// reply's fixed part plus whatever variable tail the operation defines.
|
|
pub const Status = extern struct {
|
|
status: i32, // 0, or a negative errno
|
|
_padding: u32 = 0,
|
|
len: u32 = 0,
|
|
_padding2: u32 = 0,
|
|
};
|
|
|
|
/// The fixed prefix every packet spends — `Header` on a request or an event,
|
|
/// `Status` on a reply. One constant, because the two are deliberately the same
|
|
/// width: the packet budget does not depend on the direction.
|
|
pub const prefix_size: usize = @sizeOf(Header);
|
|
|
|
comptime {
|
|
if (@sizeOf(Header) != 16 or @sizeOf(Status) != 16)
|
|
@compileError("the envelope prefix is 16 bytes in both directions");
|
|
}
|
|
|
|
// --- the reserved verbs -----------------------------------------------------
|
|
|
|
/// Reserved verbs, answered by every provider. `Define` numbers a protocol's
|
|
/// own verbs from `first_protocol_operation`, so no protocol can reach in here.
|
|
pub const operation_describe: u32 = 0; // -> protocol name, version, target kinds
|
|
pub const operation_enumerate: u32 = 1; // -> the current targets, one per reply page
|
|
pub const operation_subscribe: u32 = 2; // capability = the subscriber's endpoint
|
|
pub const operation_unsubscribe: u32 = 3;
|
|
pub const first_protocol_operation: u32 = 16;
|
|
|
|
/// The `describe` reply's fixed part, followed inline by `name_len` bytes of the
|
|
/// protocol's name. This is the version handshake: the version is asked for
|
|
/// once, at connect time, rather than re-carried by every packet out of a
|
|
/// 256-byte budget.
|
|
pub const Description = extern struct {
|
|
version: u32,
|
|
operation_count: u32,
|
|
event_count: u32,
|
|
name_len: u32,
|
|
};
|
|
|
|
/// Longest protocol name a `describe` reply can carry.
|
|
pub const name_maximum: usize = packet_maximum - prefix_size - @sizeOf(Description);
|
|
|
|
// --- the transport floor ----------------------------------------------------
|
|
|
|
/// The packet budget every protocol may assume on *any* transport. These are
|
|
/// the kernel-ipc transport's limits — `MESSAGE_MAXIMUM` and `POST_MAXIMUM` in
|
|
/// system/kernel/ipc-synchronous.zig — restated here because the kernel keeps
|
|
/// them private and a protocol has to compile against something. A fatter
|
|
/// transport raises its own ceiling; the floor does not move, so a protocol
|
|
/// that fits here fits everywhere (communication.md: ceilings are transport
|
|
/// properties, the floor is the protocol's contract).
|
|
pub const packet_maximum: usize = 256; // one request or one reply (ipc_call)
|
|
pub const post_maximum: usize = 64; // one event packet (ipc_send)
|
|
|
|
/// Whether a request or reply whose fixed part is `T` fits the call floor once
|
|
/// the prefix is counted. The folded rule in one line: `prefix_size` is added
|
|
/// exactly once, because `T` describes only what follows it. Exported so the
|
|
/// rule itself is testable — `Define` enforces it as a compile error.
|
|
pub fn fitsPacket(comptime T: type) bool {
|
|
return prefix_size + @sizeOf(T) <= packet_maximum;
|
|
}
|
|
|
|
/// The same, against the much smaller push floor an event packet lives within.
|
|
pub fn fitsPost(comptime T: type) bool {
|
|
return prefix_size + @sizeOf(T) <= post_maximum;
|
|
}
|
|
|
|
// --- reply statuses the envelope itself produces -----------------------------
|
|
|
|
/// Continued from the kernel's danos-native errno numbering
|
|
/// (system/kernel/ipc-synchronous.zig, which ends at `EPERM` = 9), so a client
|
|
/// reads one vocabulary whether the number came from the kernel or a provider.
|
|
/// Positive here, sent negated in `Status.status`, as the kernel spells it.
|
|
pub const ENOSYS: i32 = 10; // this protocol has no such operation
|
|
pub const EPROTO: i32 = 11; // malformed packet: shorter than the verb it names
|
|
pub const EBUSY: i32 = 12; // the thing asked for is held by someone still alive
|
|
|
|
/// Restated from the kernel's half of the numbering, because a provider refuses
|
|
/// too and userspace has no other place to read these from: `ENOENT` is "no such
|
|
/// name", `EPERM` "not permitted". The protocol registry answers an ungranted
|
|
/// bind with the second and a name a live provider already holds with `EBUSY`.
|
|
pub const ENOENT: i32 = 4;
|
|
pub const ENOSPC: i32 = 5;
|
|
pub const EPERM: i32 = 9;
|
|
|
|
// --- framing ----------------------------------------------------------------
|
|
|
|
/// The header of a received packet, or null when it is too short to have one.
|
|
pub fn headerOf(packet: []const u8) ?Header {
|
|
if (packet.len < prefix_size) return null;
|
|
return std.mem.bytesToValue(Header, packet[0..prefix_size]);
|
|
}
|
|
|
|
/// The status of a received reply, or null when it is too short to have one.
|
|
pub fn statusOf(packet: []const u8) ?Status {
|
|
if (packet.len < prefix_size) return null;
|
|
return std.mem.bytesToValue(Status, packet[0..prefix_size]);
|
|
}
|
|
|
|
/// Frame a bare `describe` request. Protocol-independent: the reserved verbs
|
|
/// are asked the same way of every provider.
|
|
pub fn encodeDescribe(buffer: []u8) ?[]u8 {
|
|
const header = Header{ .operation = operation_describe };
|
|
return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer);
|
|
}
|
|
|
|
/// A decoded `describe` reply: the fixed part, plus the name that follows it.
|
|
pub const Described = struct {
|
|
description: Description,
|
|
name: []const u8,
|
|
};
|
|
|
|
/// Decode a `describe` reply packet. Null if it failed, was truncated, or is
|
|
/// not a description at all.
|
|
pub fn decodeDescribe(packet: []const u8) ?Described {
|
|
const status = statusOf(packet) orelse return null;
|
|
if (status.status != 0) return null;
|
|
const body = packet[prefix_size..];
|
|
if (body.len < @sizeOf(Description)) return null;
|
|
const description = std.mem.bytesToValue(Description, body[0..@sizeOf(Description)]);
|
|
const name = body[@sizeOf(Description)..];
|
|
if (name.len < description.name_len) return null;
|
|
return .{ .description = description, .name = name[0..description.name_len] };
|
|
}
|
|
|
|
/// The single framing point: prefix, then the fixed part, then the variable
|
|
/// tail, contiguous in one buffer. Null when the packet would not fit — a
|
|
/// packet is never split, so not fitting is a failure, not a continuation.
|
|
fn frame(prefix: []const u8, fixed: []const u8, tail: []const u8, buffer: []u8) ?[]u8 {
|
|
const total = prefix.len + fixed.len + tail.len;
|
|
if (total > buffer.len) return null;
|
|
@memcpy(buffer[0..prefix.len], prefix);
|
|
@memcpy(buffer[prefix.len..][0..fixed.len], fixed);
|
|
@memcpy(buffer[prefix.len + fixed.len ..][0..tail.len], tail);
|
|
return buffer[0..total];
|
|
}
|
|
|
|
/// The bytes of a fixed part — empty for `void`, which is how an operation says
|
|
/// "nothing but the verb".
|
|
fn bytesOf(comptime T: type, value: *const T) []const u8 {
|
|
if (@sizeOf(T) == 0) return &.{};
|
|
return @as([*]const u8, @ptrCast(value))[0..@sizeOf(T)];
|
|
}
|
|
|
|
/// Read a fixed part out of a packet body. A zero-sized part always succeeds
|
|
/// (there is nothing to be short of); anything else needs its full width.
|
|
fn valueOf(comptime T: type, body: []const u8) ?T {
|
|
if (@sizeOf(T) == 0) return @as(T, undefined);
|
|
if (body.len < @sizeOf(T)) return null;
|
|
return std.mem.bytesToValue(T, body[0..@sizeOf(T)]);
|
|
}
|
|
|
|
// --- the specification ------------------------------------------------------
|
|
|
|
/// One verb of a protocol. `request` and `reply` describe the bytes *after* the
|
|
/// prefix; either may be `void`, meaning the verb (and its target) says it all.
|
|
pub const OperationSpecification = struct {
|
|
name: []const u8,
|
|
request: type = void,
|
|
reply: type = void,
|
|
};
|
|
|
|
/// One event a provider pushes to its subscribers. `payload` is the bytes after
|
|
/// the `Header`, and the whole packet must fit the push floor.
|
|
pub const EventSpecification = struct {
|
|
name: []const u8,
|
|
payload: type = void,
|
|
};
|
|
|
|
/// What `Define` is given: the contract, whole.
|
|
pub const Specification = struct {
|
|
/// The contract's name — the same word as its `/protocol/<name>` leaf and
|
|
/// its `library/protocol/` module.
|
|
name: []const u8,
|
|
version: u32,
|
|
operations: []const OperationSpecification = &.{},
|
|
events: []const EventSpecification = &.{},
|
|
};
|
|
|
|
const reserved_names = [_][]const u8{ "describe", "enumerate", "subscribe", "unsubscribe" };
|
|
|
|
fn isReservedName(comptime name: []const u8) bool {
|
|
for (reserved_names) |reserved| {
|
|
if (std.mem.eql(u8, reserved, name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Refuse a protocol type that carries the prefix inside itself. The header is
|
|
/// folded into every packet, so a type that also holds one would send it twice
|
|
/// and re-invent per-protocol addressing — the mistake the envelope exists to
|
|
/// prevent.
|
|
fn rejectStacking(comptime protocol: []const u8, comptime verb: []const u8, comptime T: type) void {
|
|
switch (@typeInfo(T)) {
|
|
.@"struct" => |info| for (info.fields) |field| {
|
|
if (field.type == Header or field.type == Status) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}', verb '{s}': the envelope prefix is folded, not stacked — " ++
|
|
"drop the {s} field '{s}' and use the packet's own Header.operation / Header.target",
|
|
.{ protocol, verb, @typeName(field.type), field.name },
|
|
));
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn nullDefault(comptime T: type) *const anyopaque {
|
|
const empty: ?T = null;
|
|
return @ptrCast(&empty);
|
|
}
|
|
|
|
// --- Define -----------------------------------------------------------------
|
|
|
|
/// Build a protocol from its specification. Everything below happens at compile
|
|
/// time; the generated type is what both sides of the conversation import.
|
|
///
|
|
/// ```zig
|
|
/// pub const Protocol = envelope.Define(.{
|
|
/// .name = "display",
|
|
/// .version = 1,
|
|
/// .operations = &.{
|
|
/// .{ .name = "configure_layer", .request = ConfigureLayer, .reply = void },
|
|
/// .{ .name = "blit", .request = Blit, .reply = void },
|
|
/// },
|
|
/// .events = &.{
|
|
/// .{ .name = "layer_lost", .payload = LayerLost },
|
|
/// },
|
|
/// });
|
|
/// ```
|
|
///
|
|
/// Refused at compile time, each with the protocol, the verb, and the numbers
|
|
/// named in the message:
|
|
///
|
|
/// - a request or reply that does not fit `packet_maximum` once `prefix_size`
|
|
/// is added (`.request = extern struct { bytes: [241]u8 }` — 241 + 16 = 257);
|
|
/// - an event payload that does not fit `post_maximum` the same way
|
|
/// (`.payload = extern struct { bytes: [49]u8 }` — 49 + 16 = 65);
|
|
/// - a type that stacks the prefix instead of folding it (a `Header` field);
|
|
/// - a verb named after a reserved one, or named twice.
|
|
///
|
|
/// A variable tail is bounded at *run* time instead, by `encodeRequest` and its
|
|
/// siblings, because only the caller knows how long it is.
|
|
pub fn Define(comptime specification: Specification) type {
|
|
comptime {
|
|
if (specification.name.len == 0) @compileError("a protocol needs a name");
|
|
if (specification.name.len > name_maximum) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}': the name is {d} bytes, and a describe reply carries at most {d}",
|
|
.{ specification.name, specification.name.len, name_maximum },
|
|
));
|
|
|
|
for (specification.operations, 0..) |operation, index| {
|
|
if (isReservedName(operation.name)) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}': '{s}' is a reserved universal verb — the envelope already answers it",
|
|
.{ specification.name, operation.name },
|
|
));
|
|
for (specification.operations[0..index]) |earlier| {
|
|
if (std.mem.eql(u8, earlier.name, operation.name)) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}': operation '{s}' is declared twice",
|
|
.{ specification.name, operation.name },
|
|
));
|
|
}
|
|
rejectStacking(specification.name, operation.name, operation.request);
|
|
rejectStacking(specification.name, operation.name, operation.reply);
|
|
if (!fitsPacket(operation.request)) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}', operation '{s}': the request is {d} bytes and the header {d}, " ++
|
|
"over the {d}-byte call floor — packets never fragment, so this has to shrink " ++
|
|
"or move its bulk to shared memory",
|
|
.{ specification.name, operation.name, @sizeOf(operation.request), prefix_size, packet_maximum },
|
|
));
|
|
if (!fitsPacket(operation.reply)) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}', operation '{s}': the reply is {d} bytes and the status header {d}, " ++
|
|
"over the {d}-byte call floor",
|
|
.{ specification.name, operation.name, @sizeOf(operation.reply), prefix_size, packet_maximum },
|
|
));
|
|
}
|
|
|
|
for (specification.events, 0..) |event, index| {
|
|
for (specification.events[0..index]) |earlier| {
|
|
if (std.mem.eql(u8, earlier.name, event.name)) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}': event '{s}' is declared twice",
|
|
.{ specification.name, event.name },
|
|
));
|
|
}
|
|
rejectStacking(specification.name, event.name, event.payload);
|
|
if (!fitsPost(event.payload)) @compileError(std.fmt.comptimePrint(
|
|
"protocol '{s}', event '{s}': the payload is {d} bytes and the header {d}, " ++
|
|
"over the {d}-byte push floor — an event carries the header too, so it is the " ++
|
|
"payload that has to give",
|
|
.{ specification.name, event.name, @sizeOf(event.payload), prefix_size, post_maximum },
|
|
));
|
|
}
|
|
}
|
|
|
|
return struct {
|
|
pub const protocol_name: []const u8 = specification.name;
|
|
pub const version: u32 = specification.version;
|
|
|
|
/// What a provider sizes its receive and reply buffers to. A packet's
|
|
/// fixed part may be far smaller, but any caller may send up to the
|
|
/// floor and a short buffer truncates rather than refuses.
|
|
pub const message_maximum: usize = packet_maximum;
|
|
|
|
/// The widest packet this protocol's fixed parts can actually produce,
|
|
/// prefix included — a diagnostic, and what a test pins.
|
|
pub const request_maximum: usize = widest(specification.operations, .request);
|
|
pub const reply_maximum: usize = widest(specification.operations, .reply);
|
|
pub const event_maximum: usize = blk: {
|
|
var widest_event: usize = prefix_size;
|
|
for (specification.events) |event| widest_event = @max(widest_event, prefix_size + @sizeOf(event.payload));
|
|
break :blk widest_event;
|
|
};
|
|
|
|
/// This protocol's verbs, numbered from `first_protocol_operation` in
|
|
/// declaration order.
|
|
pub const Operation = numbered(specification.operations, "name");
|
|
|
|
/// This protocol's events, numbered from `first_protocol_operation` in
|
|
/// their **own** space. Events travel only provider → subscriber over
|
|
/// `ipc_send` and operations only client → provider over `ipc_call`, so
|
|
/// the direction already tells the two apart; separate spaces mean
|
|
/// appending an operation can never renumber a shipped event.
|
|
pub const Event = numbered(specification.events, "name");
|
|
|
|
/// The bytes after the `Header` on a request for `operation`.
|
|
pub fn RequestOf(comptime operation: Operation) type {
|
|
return specification.operations[indexOf(@intFromEnum(operation))].request;
|
|
}
|
|
|
|
/// The bytes after the `Status` on the reply to `operation`.
|
|
pub fn ReplyOf(comptime operation: Operation) type {
|
|
return specification.operations[indexOf(@intFromEnum(operation))].reply;
|
|
}
|
|
|
|
/// The bytes after the `Header` on an `event` packet.
|
|
pub fn PayloadOf(comptime event: Event) type {
|
|
return specification.events[indexOf(@intFromEnum(event))].payload;
|
|
}
|
|
|
|
// --- client side ----------------------------------------------------
|
|
|
|
/// Frame `[Header][request][tail]`. `tail` is the variable part (a path,
|
|
/// write bytes); pass `&.{}` when the verb has none. Null if the packet
|
|
/// would exceed the buffer or the call floor.
|
|
pub fn encodeRequest(
|
|
comptime operation: Operation,
|
|
target: u64,
|
|
request: RequestOf(operation),
|
|
tail: []const u8,
|
|
buffer: []u8,
|
|
) ?[]u8 {
|
|
const header = Header{ .operation = @intFromEnum(operation), .target = target };
|
|
const packet = frame(std.mem.asBytes(&header), bytesOf(RequestOf(operation), &request), tail, buffer) orelse return null;
|
|
return if (packet.len > packet_maximum) null else packet;
|
|
}
|
|
|
|
/// Frame `[Status][reply][tail]` — the provider's answer, for a provider
|
|
/// that composes its own reply rather than using `Provider.dispatch`.
|
|
pub fn encodeReply(
|
|
comptime operation: Operation,
|
|
status: i32,
|
|
reply: ReplyOf(operation),
|
|
tail: []const u8,
|
|
buffer: []u8,
|
|
) ?[]u8 {
|
|
const fixed = bytesOf(ReplyOf(operation), &reply);
|
|
const head = Status{ .status = status, .len = @intCast(fixed.len + tail.len) };
|
|
const packet = frame(std.mem.asBytes(&head), fixed, tail, buffer) orelse return null;
|
|
return if (packet.len > packet_maximum) null else packet;
|
|
}
|
|
|
|
/// Frame `[Header][payload]` for an asynchronous push. Null if it would
|
|
/// exceed the buffer or the push floor — an event that does not fit is
|
|
/// dropped at the source, never split.
|
|
pub fn encodeEvent(
|
|
comptime event: Event,
|
|
target: u64,
|
|
payload: PayloadOf(event),
|
|
buffer: []u8,
|
|
) ?[]u8 {
|
|
const header = Header{ .operation = @intFromEnum(event), .target = target };
|
|
const packet = frame(std.mem.asBytes(&header), bytesOf(PayloadOf(event), &payload), &.{}, buffer) orelse return null;
|
|
return if (packet.len > post_maximum) null else packet;
|
|
}
|
|
|
|
/// Which of this protocol's verbs a packet names — null for a reserved
|
|
/// verb, or for a number this protocol does not define.
|
|
pub fn operationOf(packet: []const u8) ?Operation {
|
|
const header = headerOf(packet) orelse return null;
|
|
const index = header.operation -% first_protocol_operation;
|
|
if (header.operation < first_protocol_operation or index >= specification.operations.len) return null;
|
|
return @enumFromInt(header.operation);
|
|
}
|
|
|
|
/// Which of this protocol's events a pushed packet carries.
|
|
pub fn eventOf(packet: []const u8) ?Event {
|
|
const header = headerOf(packet) orelse return null;
|
|
const index = header.operation -% first_protocol_operation;
|
|
if (header.operation < first_protocol_operation or index >= specification.events.len) return null;
|
|
return @enumFromInt(header.operation);
|
|
}
|
|
|
|
/// The fixed request part of a packet already known to name `operation`.
|
|
pub fn decodeRequest(comptime operation: Operation, packet: []const u8) ?RequestOf(operation) {
|
|
if (packet.len < prefix_size) return null;
|
|
return valueOf(RequestOf(operation), packet[prefix_size..]);
|
|
}
|
|
|
|
/// The bytes after the fixed request part — empty when there are none.
|
|
pub fn requestTail(comptime operation: Operation, packet: []const u8) []const u8 {
|
|
const start = prefix_size + @sizeOf(RequestOf(operation));
|
|
return if (packet.len <= start) &.{} else packet[start..];
|
|
}
|
|
|
|
/// The fixed reply part of a reply packet. Null on a short packet; the
|
|
/// caller checks `statusOf(packet).status` for the provider's verdict.
|
|
pub fn decodeReply(comptime operation: Operation, packet: []const u8) ?ReplyOf(operation) {
|
|
if (packet.len < prefix_size) return null;
|
|
return valueOf(ReplyOf(operation), packet[prefix_size..]);
|
|
}
|
|
|
|
/// The bytes after the fixed reply part, clipped to what `Status.len`
|
|
/// says actually arrived.
|
|
pub fn replyTail(comptime operation: Operation, packet: []const u8) []const u8 {
|
|
const status = statusOf(packet) orelse return &.{};
|
|
const start = prefix_size + @sizeOf(ReplyOf(operation));
|
|
const end = @min(packet.len, prefix_size + @as(usize, status.len));
|
|
return if (end <= start) &.{} else packet[start..end];
|
|
}
|
|
|
|
/// The payload of a pushed packet already known to carry `event`.
|
|
pub fn decodeEvent(comptime event: Event, packet: []const u8) ?PayloadOf(event) {
|
|
if (packet.len < prefix_size) return null;
|
|
return valueOf(PayloadOf(event), packet[prefix_size..]);
|
|
}
|
|
|
|
// --- provider side --------------------------------------------------
|
|
|
|
/// This protocol's dispatch table, bound to the provider's own state
|
|
/// type. `describe` is answered here, from the specification; every verb
|
|
/// this provider left null answers `-ENOSYS`, which is what makes the
|
|
/// reserved verbs mean the same thing at every provider in the system.
|
|
///
|
|
/// ```zig
|
|
/// const Serve = Protocol.Provider(*Server);
|
|
/// const handlers = Serve.Handlers{ .blit = onBlit, .configure_layer = onConfigureLayer };
|
|
/// const reply_len = Serve.dispatch(server, handlers, message, sender, capability, reply);
|
|
/// ```
|
|
///
|
|
/// A handler returns the number of `answer.tail()` bytes it wrote, or a
|
|
/// negative errno.
|
|
pub fn Provider(comptime Context: type) type {
|
|
return struct {
|
|
/// A reserved verb a provider chooses to implement itself.
|
|
/// `enumerate` writes its targets into the tail; `subscribe`
|
|
/// takes the subscriber's endpoint from `invocation.capability`.
|
|
pub const ReservedHandler = *const fn (Context, Invocation(void), Answer(void)) isize;
|
|
|
|
/// One optional handler per verb, named exactly as the verb,
|
|
/// plus the reserved verbs the envelope cannot answer alone.
|
|
pub const Handlers = handlerTable(Context);
|
|
|
|
/// Answer one received packet: writes `[Status][reply][tail]`
|
|
/// into `reply` and returns its length. Zero means the reply
|
|
/// buffer could not even hold a status, so nothing was written.
|
|
pub fn dispatch(
|
|
context: Context,
|
|
handlers: Handlers,
|
|
packet: []const u8,
|
|
sender: u32,
|
|
capability: ?usize,
|
|
reply: []u8,
|
|
) usize {
|
|
if (reply.len < prefix_size) return 0;
|
|
const header = headerOf(packet) orelse return refuse(reply, -EPROTO);
|
|
const body = packet[prefix_size..];
|
|
|
|
if (header.operation == operation_describe) return describeInto(reply);
|
|
|
|
inline for (specification.operations, 0..) |operation, index| {
|
|
if (header.operation == first_protocol_operation + index) {
|
|
return invoke(
|
|
Context,
|
|
operation.request,
|
|
operation.reply,
|
|
@field(handlers, operation.name),
|
|
context,
|
|
header.target,
|
|
body,
|
|
sender,
|
|
capability,
|
|
reply,
|
|
);
|
|
}
|
|
}
|
|
|
|
const reserved: ?ReservedHandler = switch (header.operation) {
|
|
operation_enumerate => handlers.enumerate,
|
|
operation_subscribe => handlers.subscribe,
|
|
operation_unsubscribe => handlers.unsubscribe,
|
|
else => null,
|
|
};
|
|
return invoke(Context, void, void, reserved, context, header.target, body, sender, capability, reply);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Shared by the protocol verbs and the reserved ones: decode, hand the
|
|
// handler a typed invocation, stamp the status. One place, so a reserved
|
|
// verb and a protocol verb behave identically.
|
|
fn invoke(
|
|
comptime Context: type,
|
|
comptime RequestType: type,
|
|
comptime ReplyType: type,
|
|
handler: ?*const fn (Context, Invocation(RequestType), Answer(ReplyType)) isize,
|
|
context: Context,
|
|
target: u64,
|
|
body: []const u8,
|
|
sender: u32,
|
|
capability: ?usize,
|
|
reply: []u8,
|
|
) usize {
|
|
const call = handler orelse return refuse(reply, -ENOSYS);
|
|
const request = valueOf(RequestType, body) orelse return refuse(reply, -EPROTO);
|
|
if (reply.len < prefix_size + @sizeOf(ReplyType)) return refuse(reply, -EPROTO);
|
|
const produced = call(context, .{
|
|
.target = target,
|
|
.request = request,
|
|
.tail = body[@min(@sizeOf(RequestType), body.len)..],
|
|
.sender = sender,
|
|
.capability = capability,
|
|
}, .{ .buffer = reply[prefix_size..] });
|
|
if (produced < 0) return refuse(reply, @intCast(produced));
|
|
return succeed(reply, @sizeOf(ReplyType) + @as(usize, @intCast(produced)));
|
|
}
|
|
|
|
fn describeInto(reply: []u8) usize {
|
|
const description = Description{
|
|
.version = specification.version,
|
|
.operation_count = specification.operations.len,
|
|
.event_count = specification.events.len,
|
|
.name_len = specification.name.len,
|
|
};
|
|
const total = @sizeOf(Description) + specification.name.len;
|
|
if (reply.len < prefix_size + total) return refuse(reply, -EPROTO);
|
|
@memcpy(reply[prefix_size..][0..@sizeOf(Description)], std.mem.asBytes(&description));
|
|
@memcpy(reply[prefix_size + @sizeOf(Description) ..][0..specification.name.len], specification.name);
|
|
return succeed(reply, total);
|
|
}
|
|
|
|
// "describe" is answered by the envelope, so it is the one reserved verb
|
|
// with no slot in the table.
|
|
const implementable_reserved = [_][]const u8{ "enumerate", "subscribe", "unsubscribe" };
|
|
|
|
fn handlerTable(comptime Context: type) type {
|
|
const count = specification.operations.len + implementable_reserved.len;
|
|
var names: [count][]const u8 = undefined;
|
|
var types: [count]type = undefined;
|
|
var attributes: [count]std.builtin.Type.StructField.Attributes = undefined;
|
|
for (specification.operations, 0..) |operation, index| {
|
|
const Handler = *const fn (Context, Invocation(operation.request), Answer(operation.reply)) isize;
|
|
names[index] = operation.name;
|
|
types[index] = ?Handler;
|
|
attributes[index] = .{ .default_value_ptr = nullDefault(Handler) };
|
|
}
|
|
const Reserved = *const fn (Context, Invocation(void), Answer(void)) isize;
|
|
for (implementable_reserved, 0..) |name, offset| {
|
|
const slot = specification.operations.len + offset;
|
|
names[slot] = name;
|
|
types[slot] = ?Reserved;
|
|
attributes[slot] = .{ .default_value_ptr = nullDefault(Reserved) };
|
|
}
|
|
const frozen_names = names;
|
|
const frozen_types = types;
|
|
const frozen_attributes = attributes;
|
|
return @Struct(.auto, null, &frozen_names, &frozen_types, &frozen_attributes);
|
|
}
|
|
};
|
|
}
|
|
|
|
// --- the provider's view of one packet --------------------------------------
|
|
|
|
/// What a provider's handler is given.
|
|
pub fn Invocation(comptime RequestType: type) type {
|
|
return struct {
|
|
/// Object addressing within this provider — the packet's `Header.target`.
|
|
target: u64,
|
|
/// The fixed request part, already decoded.
|
|
request: RequestType,
|
|
/// The bytes after it: a path, write data, a name.
|
|
tail: []const u8,
|
|
/// The kernel-stamped badge of the caller. The only source identity
|
|
/// there is — no protocol defines a sender field — so per-client state
|
|
/// is keyed on this.
|
|
sender: u32,
|
|
/// A capability the call carried: a subscriber's endpoint, a DMA
|
|
/// region. Only the synchronous call path can move one.
|
|
capability: ?usize,
|
|
};
|
|
}
|
|
|
|
/// Where a provider's handler writes its answer. The `Status` in front of it is
|
|
/// the dispatcher's to stamp — a handler never writes its own.
|
|
pub fn Answer(comptime ReplyType: type) type {
|
|
return struct {
|
|
buffer: []u8,
|
|
|
|
const fixed_size = @sizeOf(ReplyType);
|
|
|
|
/// Write the fixed reply part. A `void` reply writes nothing.
|
|
pub fn set(self: @This(), reply: ReplyType) void {
|
|
if (fixed_size == 0) return;
|
|
@memcpy(self.buffer[0..fixed_size], bytesOf(ReplyType, &reply));
|
|
}
|
|
|
|
/// Room for the variable tail; the handler returns how much of it it used.
|
|
pub fn tail(self: @This()) []u8 {
|
|
return self.buffer[fixed_size..];
|
|
}
|
|
};
|
|
}
|
|
|
|
fn refuse(reply: []u8, status: i32) usize {
|
|
const head = Status{ .status = status, .len = 0 };
|
|
@memcpy(reply[0..prefix_size], std.mem.asBytes(&head));
|
|
return prefix_size;
|
|
}
|
|
|
|
fn succeed(reply: []u8, len: usize) usize {
|
|
const head = Status{ .status = 0, .len = @intCast(len) };
|
|
@memcpy(reply[0..prefix_size], std.mem.asBytes(&head));
|
|
return prefix_size + len;
|
|
}
|
|
|
|
/// The widest `[prefix][fixed]` over a set of operations, on one side.
|
|
fn widest(comptime operations: []const OperationSpecification, comptime side: enum { request, reply }) usize {
|
|
var found: usize = prefix_size;
|
|
for (operations) |operation| {
|
|
const size = switch (side) {
|
|
.request => @sizeOf(operation.request),
|
|
.reply => @sizeOf(operation.reply),
|
|
};
|
|
found = @max(found, prefix_size + size);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/// An enum over `specifications`, tagged by their `name` field, numbered from
|
|
/// `first_protocol_operation` in declaration order.
|
|
fn numbered(comptime specifications: anytype, comptime field: []const u8) type {
|
|
var names: [specifications.len][]const u8 = undefined;
|
|
var values: [specifications.len]u32 = undefined;
|
|
for (specifications, 0..) |specification, index| {
|
|
names[index] = @field(specification, field);
|
|
values[index] = first_protocol_operation + index;
|
|
}
|
|
const frozen_names = names;
|
|
const frozen_values = values;
|
|
return @Enum(u32, .exhaustive, &frozen_names, &frozen_values);
|
|
}
|
|
|
|
/// A verb's position in its declaration list, from its wire number.
|
|
fn indexOf(comptime operation: u32) usize {
|
|
return operation - first_protocol_operation;
|
|
}
|
|
|
|
// --- tests ------------------------------------------------------------------
|
|
|
|
const testing = std.testing;
|
|
|
|
const Produce = extern struct { count: u32, flags: u32 = 0 };
|
|
const Produced = extern struct { total: u64 };
|
|
const Changed = extern struct { kind: u32, value: u32 };
|
|
|
|
const Sample = Define(.{
|
|
.name = "sample",
|
|
.version = 3,
|
|
.operations = &.{
|
|
.{ .name = "produce", .request = Produce, .reply = Produced },
|
|
.{ .name = "reset" },
|
|
},
|
|
.events = &.{
|
|
.{ .name = "changed", .payload = Changed },
|
|
},
|
|
});
|
|
|
|
test "the prefix is 16 bytes in both directions" {
|
|
try testing.expectEqual(@as(usize, 16), @sizeOf(Header));
|
|
try testing.expectEqual(@as(usize, 16), @sizeOf(Status));
|
|
try testing.expectEqual(@as(usize, 16), prefix_size);
|
|
try testing.expectEqual(@as(usize, 256), packet_maximum);
|
|
try testing.expectEqual(@as(usize, 64), post_maximum);
|
|
}
|
|
|
|
test "verb numbering skips the reserved range" {
|
|
try testing.expectEqual(@as(u32, 16), first_protocol_operation);
|
|
try testing.expectEqual(@as(u32, 16), @intFromEnum(Sample.Operation.produce));
|
|
try testing.expectEqual(@as(u32, 17), @intFromEnum(Sample.Operation.reset));
|
|
// Events are numbered in their own space, so appending an operation can
|
|
// never renumber a shipped event.
|
|
try testing.expectEqual(@as(u32, 16), @intFromEnum(Sample.Event.changed));
|
|
for ([_]u32{ operation_describe, operation_enumerate, operation_subscribe, operation_unsubscribe }) |reserved| {
|
|
try testing.expect(reserved < first_protocol_operation);
|
|
}
|
|
}
|
|
|
|
test "request round trip, header folded and tail carried" {
|
|
var buffer: [packet_maximum]u8 = undefined;
|
|
const packet = Sample.encodeRequest(.produce, 42, .{ .count = 7 }, "tail bytes", &buffer).?;
|
|
try testing.expectEqual(prefix_size + @sizeOf(Produce) + "tail bytes".len, packet.len);
|
|
|
|
const header = headerOf(packet).?;
|
|
try testing.expectEqual(@as(u32, 16), header.operation);
|
|
try testing.expectEqual(@as(u64, 42), header.target);
|
|
try testing.expectEqual(Sample.Operation.produce, Sample.operationOf(packet).?);
|
|
|
|
const request = Sample.decodeRequest(.produce, packet).?;
|
|
try testing.expectEqual(@as(u32, 7), request.count);
|
|
try testing.expectEqualStrings("tail bytes", Sample.requestTail(.produce, packet));
|
|
}
|
|
|
|
test "reply round trip" {
|
|
var buffer: [packet_maximum]u8 = undefined;
|
|
const packet = Sample.encodeReply(.produce, 0, .{ .total = 99 }, "more", &buffer).?;
|
|
const status = statusOf(packet).?;
|
|
try testing.expectEqual(@as(i32, 0), status.status);
|
|
try testing.expectEqual(@as(u32, @sizeOf(Produced) + "more".len), status.len);
|
|
try testing.expectEqual(@as(u64, 99), Sample.decodeReply(.produce, packet).?.total);
|
|
try testing.expectEqualStrings("more", Sample.replyTail(.produce, packet));
|
|
}
|
|
|
|
test "a void request and reply carry nothing but the verb" {
|
|
var buffer: [packet_maximum]u8 = undefined;
|
|
const packet = Sample.encodeRequest(.reset, 0, {}, &.{}, &buffer).?;
|
|
try testing.expectEqual(prefix_size, packet.len);
|
|
try testing.expectEqual(Sample.Operation.reset, Sample.operationOf(packet).?);
|
|
try testing.expectEqual(@as(usize, 0), Sample.requestTail(.reset, packet).len);
|
|
}
|
|
|
|
test "event round trip within the push floor" {
|
|
var buffer: [post_maximum]u8 = undefined;
|
|
const packet = Sample.encodeEvent(.changed, 0, .{ .kind = 1, .value = 2 }, &buffer).?;
|
|
try testing.expectEqual(prefix_size + @sizeOf(Changed), packet.len);
|
|
try testing.expect(packet.len <= post_maximum);
|
|
try testing.expectEqual(Sample.Event.changed, Sample.eventOf(packet).?);
|
|
try testing.expectEqual(@as(u32, 2), Sample.decodeEvent(.changed, packet).?.value);
|
|
}
|
|
|
|
// A provider over a trivial context, to drive the generated dispatch table.
|
|
const Counter = struct {
|
|
total: u64 = 0,
|
|
|
|
fn onProduce(self: *Counter, invocation: Invocation(Produce), answer: Answer(Produced)) isize {
|
|
self.total += invocation.request.count;
|
|
answer.set(.{ .total = self.total });
|
|
const note = "counted";
|
|
@memcpy(answer.tail()[0..note.len], note);
|
|
return note.len;
|
|
}
|
|
};
|
|
|
|
const CounterProvider = Sample.Provider(*Counter);
|
|
|
|
test "dispatch reaches a handler and stamps the status" {
|
|
var counter = Counter{};
|
|
const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };
|
|
|
|
var request: [packet_maximum]u8 = undefined;
|
|
const packet = Sample.encodeRequest(.produce, 0, .{ .count = 5 }, &.{}, &request).?;
|
|
var reply: [packet_maximum]u8 = undefined;
|
|
const len = CounterProvider.dispatch(&counter, handlers, packet, 3, null, &reply);
|
|
|
|
const answered = reply[0..len];
|
|
try testing.expectEqual(@as(i32, 0), statusOf(answered).?.status);
|
|
try testing.expectEqual(@as(u64, 5), Sample.decodeReply(.produce, answered).?.total);
|
|
try testing.expectEqualStrings("counted", Sample.replyTail(.produce, answered));
|
|
}
|
|
|
|
test "describe is answered by the envelope, not the provider" {
|
|
var counter = Counter{};
|
|
const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };
|
|
|
|
var request: [packet_maximum]u8 = undefined;
|
|
const packet = encodeDescribe(&request).?;
|
|
var reply: [packet_maximum]u8 = undefined;
|
|
const len = CounterProvider.dispatch(&counter, handlers, packet, 3, null, &reply);
|
|
|
|
const described = decodeDescribe(reply[0..len]).?;
|
|
try testing.expectEqualStrings("sample", described.name);
|
|
try testing.expectEqual(@as(u32, 3), described.description.version);
|
|
try testing.expectEqual(@as(u32, 2), described.description.operation_count);
|
|
try testing.expectEqual(@as(u32, 1), described.description.event_count);
|
|
}
|
|
|
|
test "an unimplemented or unknown verb answers -ENOSYS" {
|
|
var counter = Counter{};
|
|
const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };
|
|
var reply: [packet_maximum]u8 = undefined;
|
|
|
|
// A verb this protocol declares but this provider left null.
|
|
var request: [packet_maximum]u8 = undefined;
|
|
const declared = Sample.encodeRequest(.reset, 0, {}, &.{}, &request).?;
|
|
var len = CounterProvider.dispatch(&counter, handlers, declared, 3, null, &reply);
|
|
try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status);
|
|
|
|
// A number no verb of this protocol wears.
|
|
const stranger = Header{ .operation = first_protocol_operation + 900 };
|
|
len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&stranger), 3, null, &reply);
|
|
try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status);
|
|
|
|
// A reserved verb the provider does not implement answers the same way.
|
|
const enumerate = Header{ .operation = operation_enumerate };
|
|
len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&enumerate), 3, null, &reply);
|
|
try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status);
|
|
}
|
|
|
|
test "a truncated packet answers -EPROTO" {
|
|
var counter = Counter{};
|
|
const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };
|
|
var reply: [packet_maximum]u8 = undefined;
|
|
|
|
// Names `produce`, but stops before the request it promises.
|
|
const header = Header{ .operation = @intFromEnum(Sample.Operation.produce) };
|
|
const len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&header), 3, null, &reply);
|
|
try testing.expectEqual(@as(i32, -EPROTO), statusOf(reply[0..len]).?.status);
|
|
}
|
|
|
|
// The size rule, exercised directly. `Define` turns exactly these predicates
|
|
// into compile errors, which a test cannot catch — so the predicate is what the
|
|
// test pins, and the boundary protocol below proves the compile-time half from
|
|
// the other side. The negative example, spelled out: giving `Define` an
|
|
// operation with `.request = extern struct { bytes: [241]u8 }`, or an event with
|
|
// `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the
|
|
// protocol, the verb, and the two numbers named in the message.
|
|
test "the floor counts the header once, and the boundary is exact" {
|
|
try testing.expect(fitsPacket(extern struct { bytes: [240]u8 }));
|
|
try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 }));
|
|
try testing.expect(fitsPost(extern struct { bytes: [48]u8 }));
|
|
try testing.expect(!fitsPost(extern struct { bytes: [49]u8 }));
|
|
try testing.expect(fitsPacket(void));
|
|
try testing.expect(fitsPost(void));
|
|
}
|
|
|
|
const WidestRequest = extern struct { bytes: [packet_maximum - prefix_size]u8 };
|
|
const WidestEvent = extern struct { bytes: [post_maximum - prefix_size]u8 };
|
|
|
|
// A protocol sitting exactly on both floors. That this compiles at all is the
|
|
// positive half of the compile-time check.
|
|
const Boundary = Define(.{
|
|
.name = "boundary",
|
|
.version = 1,
|
|
.operations = &.{.{ .name = "fill", .request = WidestRequest, .reply = WidestRequest }},
|
|
.events = &.{.{ .name = "filled", .payload = WidestEvent }},
|
|
});
|
|
|
|
test "a protocol may sit exactly on the floor" {
|
|
try testing.expectEqual(packet_maximum, Boundary.request_maximum);
|
|
try testing.expectEqual(packet_maximum, Boundary.reply_maximum);
|
|
try testing.expectEqual(post_maximum, Boundary.event_maximum);
|
|
|
|
var buffer: [packet_maximum]u8 = undefined;
|
|
const packet = Boundary.encodeRequest(.fill, 0, .{ .bytes = @splat(0xAB) }, &.{}, &buffer).?;
|
|
try testing.expectEqual(packet_maximum, packet.len);
|
|
try testing.expectEqual(@as(u8, 0xAB), Boundary.decodeRequest(.fill, packet).?.bytes[239]);
|
|
|
|
// One byte of tail past the floor is refused at run time, not truncated.
|
|
try testing.expect(Boundary.encodeRequest(.fill, 0, .{ .bytes = @splat(0) }, "x", &buffer) == null);
|
|
|
|
var post: [post_maximum]u8 = undefined;
|
|
const event = Boundary.encodeEvent(.filled, 0, .{ .bytes = @splat(1) }, &post).?;
|
|
try testing.expectEqual(post_maximum, event.len);
|
|
}
|
|
|
|
test "a protocol's own sizes are reported prefix-included" {
|
|
try testing.expectEqual(prefix_size + @sizeOf(Produce), Sample.request_maximum);
|
|
try testing.expectEqual(prefix_size + @sizeOf(Produced), Sample.reply_maximum);
|
|
try testing.expectEqual(prefix_size + @sizeOf(Changed), Sample.event_maximum);
|
|
try testing.expectEqual(packet_maximum, Sample.message_maximum);
|
|
try testing.expectEqualStrings("sample", Sample.protocol_name);
|
|
}
|