The registry
Single-page view · as markdown
Registry overview
Peios / Developing for Peios / The registry
LCS — the Layered Configuration Subsystem — is Peios's registry: a kernel-mediated, hierarchical, secured configuration store. If you have used the Windows registry it will feel familiar — a tree of keys holding typed values — but LCS adds one defining idea: layers. This section teaches the client API; registry.h is the exhaustive reference.
Keys and values #
A key is a node in the tree. It has an immutable GUID identity, a KACS security descriptor that governs who can read or change it, and it holds values and child keys. You address a key by path and open it for specific KEY_* rights, getting back a key fd whose granted access is fixed for its lifetime.
A value is a named, typed piece of data on a key (REG_SZ, REG_DWORD, REG_BINARY, and the rest). An empty name is the key's default value.
Layers and precedence — the LCS idea #
Here is what makes LCS more than a key/value store. Every value write is tagged with a layer, and layers have a fixed precedence order. When you read a value, LCS resolves the effective entry — the one from the highest-precedence layer that has something to say — and that's what you get back.
This is what lets configuration compose cleanly:
- A base layer ships default configuration.
- A site or policy layer overlays organisation-wide settings.
- A machine-local layer overrides per-host.
They all coexist on the same key. Reading gives you the winner; writing targets a specific layer (or the base layer by default). And because it's layers rather than destructive overwrites, removing a higher layer's entry lets the lower one re-emerge — you can override and then un-override without losing the original.
Tombstones are the tool for "hide, don't delete": a per-value tombstone masks lower layers for one value, and a blanket tombstone masks all lower values of a key on a layer at once. Keys have the same idea via hide.
Reads report where the answer came from #
Because a value can come from any layer, the read tells you which layer won and gives you a sequence number for the effective entry. That sequence number is the basis of safe updates: pass it back as a compare-and-swap guard on a write, and the write only lands if nothing changed underneath you.
Transactions #
Mutating operations — creating keys, setting and deleting values — can be grouped into a transaction and committed atomically, so a multi-step configuration change either fully applies or not at all. Transactions are abort-by-default: close the transaction fd without committing and nothing happens. See watching and transactions.
Watches #
You can ask a key to notify you when it changes — values, subkeys, or its security — optionally across its whole subtree. The elegant part: once armed, the key fd itself becomes pollable, so a registry watch drops straight into an epoll loop with no side channel.
The client, not the source #
This SDK is the registry client — it reads and writes the store. It does not implement a registry source (a storage backend); that's a separate library, librsi. As a client you speak only the calls in registry.h.
Where to go in this section #
- Reading and writing — open a key, read effective values, write to layers, do safe updates.
- Watching and transactions — react to changes and apply atomic multi-step edits.
registry.hreference — every call in full.
Reading and writing
Peios / Developing for Peios / The registry
This guide covers the bread-and-butter registry operations. The full call signatures and error sets are in registry.h; here we string them together. Recall the registry's descriptor-struct buffer convention: reads fill *_cap/*_len fields and a zero-capacity buffer probes.
The fragments below elide some error checks for brevity — every call returns -1 with errno on failure, and real code must check each one (see Library conventions).
Opening a key #
Open a key for the rights you need:
int key = ;
if
parent_fd == -1 means path is absolute. To open relative to a key you already hold, pass that key fd as the parent. Use peios_reg_create_key instead when the key might not exist yet — it opens-or-creates and reports which happened.
Reading the effective value #
Reading resolves layer precedence for you and hands back the winning value, its type, and which layer it came from:
struct peios_reg_value v = ;
unsigned char data;
v.data = data; v.data_cap = sizeof data;
/* leave v.layer NULL if you don't care which layer won */
if else if else if
The name_len is explicit (value names are length-counted; 0 reads the key's default value). Pass a transaction fd as the fourth argument to read within a transaction, or -1 for none.
To read every value at once, use peios_reg_query_values_batch — one call fills a buffer with all effective values in a packed record format. To walk them one at a time, loop peios_reg_enum_value from index 0 until ENOENT.
Writing a value #
Writes target a specific layer. Pass NULL/0 for the layer to write the base layer:
uint32_t timeout = 30;
int rc = ; /* no CAS guard */
Writing to a higher-precedence layer overrides lower ones without destroying them; deleting that layer's entry later lets the lower value re-emerge.
Safe updates with compare-and-swap #
To read-modify-write without clobbering a concurrent change, feed the sequence you read back as the expected_seq guard on the write. The write only lands if nothing changed underneath you; otherwise it fails with EAGAIN and you retry:
for
Passing expected_seq == 0 disables the guard (an unconditional write).
Cleaning up #
Close key fds with close() when done. Values you wrote with auto-commit (txn_fd == -1) are already durable to the layer; to force the source to persist a hive's pending writes at a known point, call peios_reg_flush.
Next #
- Watching and transactions — react to changes and batch atomic edits.
registry.hreference — every call, field, and error.
Watching and transactions
Peios / Developing for Peios / The registry
Beyond simple reads and writes, LCS gives you two higher-order tools: watches to react to change, and transactions to apply several edits atomically. Both are in registry.h; this guide shows the shape of using them. To keep that shape visible, the fragments elide most error checks — every call returns -1 with errno on failure, and real code must check each one (see Library conventions).
Watching for changes #
Ask a key to notify you when it changes, then poll its fd. Because the armed key fd is itself pollable, a watch integrates directly into whatever event loop you already run:
int key = ;
/* Watch values and subkeys, across the whole subtree. */
;
struct pollfd pfd = ;
for
filter is a mask of REG_NOTIFY_VALUE, REG_NOTIFY_SUBKEY, and REG_NOTIFY_SD; REG_NOTIFY_ALL covers all three. The subtree flag extends the watch to descendants. Arming needs KEY_NOTIFY on the key. Call peios_reg_notify(key, 0, 0) to disarm.
Each read() drains as many complete change records as fit — every record starts [total_len: u32][event_type: u16][name_len: u16][name], so you step through the buffer by total_len. The full layout, the REG_WATCH_* event types, and the extra path fields a subtree watch appends are in the reference. Two practical notes: a buffer too small for even one record fails EINVAL, so size it generously rather than exactly; and a REG_WATCH_OVERFLOW record means events were dropped — re-read the key's state instead of trusting the stream.
This is how a service picks up configuration changes live — no polling loop re-reading values on a timer, just a blocking poll that wakes when something actually changed.
Transactions #
When a configuration change spans several operations — create a key, set a few values, delete a stale one — you usually want it to be all-or-nothing. That's a transaction.
Begin one, pass its fd as the txn_fd argument to each operation you want enlisted, then commit:
int txn = ;
int key = ;
uint32_t one = 1;
;
;
int rc = ;
if else if
;
;
Two things to keep in mind:
- Abort is the default. If you close the transaction fd without committing — including on any early-return error path — nothing is applied. So you don't need explicit rollback logic; just don't commit.
- Commit can be retried.
EBUSY(write-lock contention) andEIO(source failure) leave the transaction active, so you can retrypeios_reg_commit. Only0(committed — the fd is now terminal) andEINVAL(already committed or never bound) are final. Check state at any point withpeios_reg_txn_status.
Backup and restore #
To snapshot a key and its whole subtree, or replace one from a snapshot, use peios_reg_backup and peios_reg_restore. They stream to and from an fd and are gated by SeBackupPrivilege / SeRestorePrivilege; restore applies in a single transaction.
Next #
registry.hreference — the notify filters, transaction states, and every error in full.- Reading and writing — the value operations you enlist in a transaction.