Registry sources
Single-page view · as markdown
Registry sources overview
Peios / Developing for Peios / Registry sources
A registry source is a storage backend for the LCS registry. It is the other half of the registry story: where a client opens keys and reads values, a source is what actually holds those keys and values and answers the kernel when it needs them. If you are implementing a place for registry data to live — a file-backed store, a database adapter, an in-memory provider for tests — you are writing a source, and librsi is the library for it.
This is a different library from libpeios, for a different audience. libpeios is the registry client; librsi is the registry source. They share the SDK's conventions and substrate, but you link librsi (-lrsi, <rsi.h>) to write a source.
How it works #
A source talks to the kernel over the RSI protocol — the Registry Source Interface — on a single file descriptor:
flowchart LR
client[Registry client<br/>libpeios] -->|syscalls| kernel[LCS in the kernel]
kernel <-->|RSI framed protocol<br/>over the source fd| source[Your source<br/>librsi]
The flow is:
- Register. Your process declares which hives (registry subtrees) it backs and registers with the kernel, receiving a source fd. This needs
SeTcbPrivilege. - Serve. The kernel sends your source requests — "look up this child", "store this value", "begin this transaction" — as framed messages you
read(2)from the source fd. You decode each one, do the work against your storage, andwrite(2)back a framed response. - Repeat until the source fd reaches EOF (the source is closing).
When a client reads or writes a key in one of your hives, the kernel turns that into RSI requests to you. Your source is the source of truth; the kernel mediates, enforces security, and resolves layer precedence on top of what you report.
The serve loop #
Every source is, at heart, this loop:
for
The three headers map onto the three steps:
<rsi/source.h>— registering and getting the source fd.<rsi/request.h>— reading and decoding requests.<rsi/response.h>— building and sending responses.
What the kernel handles, and what you handle #
The division of labour matters, because it keeps a source simple:
- The kernel handles security (access checks against key SDs), layer precedence resolution, transaction coordination across sources, and the client-facing syscall surface. It also assigns the global sequence numbers.
- Your source handles durable storage: it stores the name→GUID entries, the key metadata records, and the layered values, and it reports them back faithfully when asked. It honours transaction boundaries (buffer, then commit or abort) and compare-and-swap guards on writes.
You do not implement precedence, access control, or the client protocol — you store data and answer questions about it. That's why a source's serve loop is mostly a dispatch table over storage operations.
Requests come in families #
The request reference groups the operations, and it helps to hold the shape in mind:
- Path/entry ops — the name→GUID hierarchy (
LOOKUP,CREATE_ENTRY,HIDE_ENTRY,DELETE_ENTRY,ENUM_CHILDREN). - Key ops — key metadata records (
CREATE_KEY,READ_KEY,DROP_KEY,WRITE_KEY). - Value ops — the typed values on a key (
QUERY_VALUES,SET_VALUE,DELETE_VALUE_ENTRY,SET_BLANKET_TOMBSTONE). - Transaction ops — atomic grouping (
BEGIN/COMMIT/ABORT_TRANSACTION). - Layer ops —
DELETE_LAYER,FLUSH.
Most reply with a simple status; five carry data back on success.
Where to go in this section #
- Registering a source — declare your hives and get the source fd.
- Serving requests — the read/parse/dispatch/decode loop in full.
- Building responses — status-only and payload-bearing replies.
Registering a source
Peios / Developing for Peios / Registry sources
Every source starts by registering. You tell the kernel which hives your process backs, and it hands you a source fd to serve on. The full detail is in rsi/source.h; this guide walks the decisions.
Declare your hives #
A hive is a registry subtree with its own root key. Fill in one struct rsi_hive per hive your source backs:
struct rsi_hive hive = ;
The two decisions per hive:
- Global or private? A global hive (
flags = 0,scope_guidzero) is visible system-wide. A private hive (flags = RSI_HIVE_PRIVATE,scope_guidnon-zero) is scoped — only tokens carrying that scope GUID in their LCS credentials can resolve it. Use a private hive for per-application or per-tenant state that shouldn't be system-visible. - The root GUID. Every hive is anchored at a root key identified by
root_guid. This is the GUID paths in the hive resolve from, and it must match the root your storage actually holds.
Register #
Hand the kernel your hive array and the highest sequence number you've already persisted:
int src = ;
if
/* `src` is the source fd — serve the RSI protocol on it. */
Two things to get right:
SeTcbPrivilegeis required. Registration is a trusted operation; without the privilege you getEPERM. Sources run as trusted system components.max_sequenceis your durability contract. It is the highest sequence number this source has already persisted. The kernel resumes its global sequence counter past it, so it never hands out a number you've used. A brand-new source with no stored state passes0; a source restarting with durable data must scan that data for the highest sequence it ever wrote and pass that. Getting this wrong risks sequence reuse, so make it the first thing your restart path computes.
You can register several hives in one call by passing an array and a count greater than one; the kernel enforces its configured MaxHivesPerSource limit (ENOSPC if you exceed it).
After registration #
The returned fd is your serve endpoint — you read(2) requests and write(2) responses on it directly (via the request and response helpers). Closing it deregisters the source and signals EOF to the kernel. Keep it open for the life of the source, and move on to the serve loop.
Next #
- Serving requests — the loop that runs on the source fd.
rsi/source.hreference — every field and error.
Serving requests
Peios / Developing for Peios / Registry sources
With a source fd in hand, a source spends its life in one loop: read a request, decode it, do the work, reply. This guide builds that loop. The exhaustive per-op detail is in rsi/request.h and rsi/response.h.
The loop skeleton #
unsigned char buf; /* size generously — a short buffer is EMSGSIZE */
for
Three details in that skeleton matter:
rsi_read_requestblocks until a request is queued, and returns0at EOF — that's your exit. Sizebufgenerously; a frame larger thanbufreturns-1/EMSGSIZE.rsi_parse_requestgives youreq.op_codeto dispatch on, plusreq.request_id(which every reply must echo — the helpers do this for you) andreq.txn_id(nonzero when the request is inside a transaction).- Always reply. Every request expects exactly one response. If you can't handle an op, reply with a non-OK status rather than dropping it.
Decoding a request #
Inside a handler, decode the payload with the matching rsi_request_* parser. For a LOOKUP:
void
And for a SET_VALUE, a status-only op:
void
The borrow rule #
Every decoded name and data field — q.child_name, v.value_name, v.data, and the rest — is a (ptr, len) pair that points into buf. Those pointers are valid only until the next rsi_read_request reuses the buffer. So:
- Consume them within the handler (store the bytes, resolve the name) before the loop comes around again, or
- Copy out anything you need to keep. Never stash a raw borrowed pointer across iterations.
GUIDs and scalars in the decoded struct are copied by value, so those are always safe to keep — it's only the borrowed (ptr, len) fields that have the lifetime.
Transactions #
When the kernel sends BEGIN_TRANSACTION, buffer subsequent writes tagged with that transaction_id instead of applying them; req.txn_id on each later request tells you which transaction it belongs to (0 = none). On COMMIT_TRANSACTION apply the buffered set atomically; on ABORT_TRANSACTION discard it. All three are status-only replies. The kernel coordinates transaction boundaries — your job is to buffer, then commit or discard on command.
Next #
- Building responses — choosing and filling the right reply.
rsi/request.hreference — every op's decoder and struct.
Building responses
Peios / Developing for Peios / Registry sources
Every request gets exactly one response. Choosing the right one is simple once you know the rule; filling it in is a matter of handing librsi flat arrays and letting it encode the frame. The full contract is in rsi/response.h; this guide is the working version.
The rule #
On failure, always
rsi_respond_status. On success,rsi_respond_statustoo — unless the op is one of the five that carry a payload.
Most operations (SET_VALUE, CREATE_KEY, the transaction ops, FLUSH, …) are status-only in both cases. And any op reports a non-OK outcome with rsi_respond_status, whatever it is:
/* Success for a status-only op: */
;
/* Any op reporting a problem: */
; /* e.g. a missing key */
; /* a failed CAS */
You never build the frame or echo the request id yourself — the helper reads what it needs from req and writes the framed reply to fd.
The five payload-bearing ops #
Exactly five ops return data on success, each with its own helper:
| Op | Helper | You supply |
|---|---|---|
LOOKUP | rsi_respond_lookup | path entries + the referenced keys' metadata |
ENUM_CHILDREN | rsi_respond_enum_children | children (name + path entries) + metadata |
READ_KEY | rsi_respond_read_key | one key's non-layered metadata |
QUERY_VALUES | rsi_respond_query_values | value entries + blanket tombstones |
DELETE_LAYER | rsi_respond_delete_layer | the orphaned keys' GUIDs |
You pass the result as flat arrays; librsi validates and heap-encodes the wire frame. A QUERY_VALUES reply, for example:
struct rsi_value_entry values = ;
;
You report what you store, per layer; the kernel resolves precedence across the layers you return. (Note the (NULL, 0) for the empty blanket array — a NULL pointer is allowed only when its count is zero.)
The validation contract #
The helpers check their inputs before encoding and return -1/EINVAL if you break the contract — better a caught programming error than a malformed frame on the wire. The rules that apply across all of them:
(ptr, len)pairs: a pointer may beNULLonly when its length/count is zero.- Booleans (
volatile_key,symlink, target types) are strictly0or1. - Hidden path targets (
RSI_PATH_TARGET_HIDDEN) carry an all-zerotarget_guid. LOOKUP/ENUM_CHILDRENmetadata must exactly cover the GUID targets in your path entries — every referenced key present, none missing, no duplicates, nothing unreferenced.DELETE_LAYERorphan GUIDs are nonzero and unique.
Beyond EINVAL, a helper can also fail with ENOMEM or EOVERFLOW (building the frame), EIO (a short write), or the raw write(2) errno — treat those as you would any I/O failure on the source fd.
Putting it together #
A source's handler for a payload-bearing op is: decode the request, gather the result from storage into the flat arrays the helper wants, call the helper. For a status-only op: decode, do the work, rsi_respond_status. On any error along the way — a decode failure, a missing key, an I/O problem — rsi_respond_status with the appropriate non-OK RSI_* code. That uniformity is what keeps a source's dispatch table readable no matter how many ops it supports.
Next #
rsi/response.hreference — every helper, struct, and error in full.- Serving requests — the loop these replies live in.