4.3 Message Framing
The protocol is binary and multiplexed. All multi-byte integers are little-endian.
4.3.1 The request header #
A request is 22 bytes of header followed by an operation-specific payload.
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | total_len |
| 4 | 8 | request_id |
| 12 | 2 | op_code |
| 14 | 8 | txn_id |
total_len is the whole message including the header.
request_id matches a response to its request. Request ids are
allocated by the kernel, are strictly increasing within one connection,
and are never reused while that connection lives — including after a
request has timed out.
txn_id is the transaction the operation belongs to, or zero for none.
When non-zero, the source MUST process the operation inside that
transaction's context.
4.3.2 The response header #
A response is 14 bytes of header followed by a payload. It carries no
txn_id.
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | total_len |
| 4 | 8 | request_id |
| 12 | 2 | op_code |
A source MUST copy request_id from the request into its response, and
MUST set op_code to the request's operation code with the high bit
set — that is, op_code | 0x8000. Every operation has a named response
code for this value.
Every response payload begins with a u32 status at offset 14, so the
minimum response size is 18 bytes.
4.3.3 Encoding #
A length-prefixed field is a u32 byte count followed by that many
bytes. Strings so encoded are UTF-8 and carry no terminator; a
terminator byte counted in the length is a null byte and is therefore
invalid. Some length-prefixed fields carry binary rather than text —
Security Descriptors and value data — and are not UTF-8.
A GUID is 16 raw bytes.
An array is a u32 count followed by that many entries.
A boolean is one byte.
4.3.4 Extension, and its asymmetry #
Requests and responses extend differently, and a source MUST implement both rules.
A request MAY carry trailing fields beyond those a source
recognises. A source MUST skip them, using total_len to find the end
of the message. This is how the kernel adds an optional field without
an RSI version bump.
A response MUST NOT. The kernel rejects any trailing bytes in a response payload as malformed data. A source MUST emit exactly the payload each operation defines, and no more. Extending the protocol in this direction is done with new operations, never by growing an existing payload.
4.3.5 Reading and writing #
The device fd is message-oriented. total_len frames messages, but a
successful I/O call never splits or joins one.
read() returns exactly one complete request. If none is queued, a
blocking read waits until one is, or until the fd is closing, in which
case it returns 0. Under O_NONBLOCK an empty queue returns EAGAIN.
If the caller's buffer is too small for the next queued request,
read() returns EMSGSIZE and does not consume it.
write() submits exactly one complete response. The buffer length
MUST equal the response header's total_len, and MUST be at least the
response header size. A successful write is never short.
The following all fail EINVAL and tear the connection down,
marking the source Down:
- a length shorter than the response header;
- a length that does not equal
total_len; - an unknown
request_id; - a response to a request that has not been delivered;
- a second response to a request already answered;
- an
op_codethat is not the request's with the response bit set; - a response written on a descriptor that is not the slot's active one.
A source that cannot frame its own messages correctly is not one whose other answers can be relied on. Ordinary per-operation errors are reported through the status vocabulary and do not tear anything down.
poll() reports the fd readable when at least one complete request
is queued, writable while the slot is Active, and POLLHUP | POLLERR
when the slot is Down or the fd is closing. An fd that is open but not
yet registered reports nothing.
4.3.6 Concurrency and timing #
A source MAY process requests in any order and MUST handle multiple
in-flight requests without head-of-line blocking. Responses are matched
by request_id, not by arrival order.
The kernel bounds in-flight requests per source by
MaxConcurrentRSIRequests, default 256.
A source MUST respond to every request it has read, exactly once,
even if the kernel-side caller has already given up. The kernel applies
a request timeout — RequestTimeoutMs, default 30 seconds, measured
from the moment it first tries to reserve an in-flight slot and
covering the whole wait — and a source is not disconnected for
exceeding it. Late responses are validated and processed exactly like
on-time ones.
A timed-out request remains in the kernel's in-flight table, and keeps occupying one of the source's slots, until the source answers or the connection is torn down. A source that accumulates unanswered requests will exhaust its own concurrency budget.