Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

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.h reference — 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 = peios_reg_open_key(-1, "/System/MyApp", KEY_READ, 0);
if (key < 0) {
    if (errno == ENOENT) { /* not there */ }
    else if (errno == EACCES) { /* not allowed */ }
    return -1;
}

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 = {0};
unsigned char data[256];
v.data = data;  v.data_cap = sizeof data;
/* leave v.layer NULL if you don't care which layer won */

if (peios_reg_query_value(key, "Timeout", 7, -1, &v) == 0) {
    /* v.type is REG_*, v.data_len bytes valid in `data`, v.sequence is
       the effective entry's sequence number. */
} else if (errno == ENOENT) {
    /* no effective value (or a tombstone masks it) */
} else if (errno == ERANGE) {
    /* data buffer too small; v.data_len holds the required size — grow & retry */
}

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 = peios_reg_set_value(key, "Timeout", 7, REG_DWORD,
                             &timeout, sizeof timeout,
                             NULL, 0,      /* base layer */
                             -1,           /* auto-commit (no transaction) */
                             0);           /* 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 (;;) {
    struct peios_reg_value v = {0};
    unsigned char buf[64]; v.data = buf; v.data_cap = sizeof buf;
    if (peios_reg_query_value(key, "Counter", 7, -1, &v) != 0)
        break;                                     /* real error — don't spin */

    uint32_t n; memcpy(&n, buf, sizeof n); n++;

    int rc = peios_reg_set_value(key, "Counter", 7, REG_DWORD,
                                 &n, sizeof n, NULL, 0, -1,
                                 v.sequence);          /* CAS on the sequence */
    if (rc == 0) break;                                /* success */
    if (errno != EAGAIN) { /* real error */ break; }   /* else: retry */
}

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.h reference — 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 = peios_reg_open_key(-1, "/System/MyApp", KEY_READ | KEY_NOTIFY, 0);

/* Watch values and subkeys, across the whole subtree. */
peios_reg_notify(key, REG_NOTIFY_VALUE | REG_NOTIFY_SUBKEY, 1 /* subtree */);

struct pollfd pfd = { .fd = key, .events = POLLIN };
for (;;) {
    poll(&pfd, 1, -1);
    if (pfd.revents & POLLIN) {
        /* read() the key fd to drain the change records, then re-read
           whatever you care about. */
        char records[512];
        ssize_t n = read(key, records, sizeof records);
        /* ... process change records ... */
    }
}

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 = peios_reg_begin_transaction();

int key = peios_reg_create_key(-1, "/System/MyApp/v2", KEY_WRITE, 0,
                               NULL /* base layer */, txn, NULL);
uint32_t one = 1;
peios_reg_set_value(key, "Enabled", 7, REG_DWORD, &one, sizeof one,
                    NULL, 0, txn, 0);
peios_reg_delete_value(key, "LegacyFlag", 10, NULL, 0, txn);

int rc = peios_reg_commit(txn);
if (rc == 0) {
    /* everything applied atomically; txn is terminal — close it */
} else if (errno == EBUSY || errno == EIO) {
    /* transient: the transaction is still active — retry the commit */
}
close(txn);
close(key);

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) and EIO (source failure) leave the transaction active, so you can retry peios_reg_commit. Only 0 (committed — the fd is now terminal) and EINVAL (already committed or never bound) are final. Check state at any point with peios_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.h reference — the notify filters, transaction states, and every error in full.
  • Reading and writing — the value operations you enlist in a transaction.

Peios Learn — documentation for the Peios project.

Built with Trail.