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

loregd

Local Registry Daemon — the SQLite-backed RSI source that provides the Machine and Users hives: storage, concurrency, and request handling.

Single-page view · as markdown

1.1 Overview

Peios / Peios technical reference / loregd / Introduction

loregd — the Local Registry Daemon — is the primary registry source for Peios. It holds one or more registry hives in SQLite databases and serves them to the kernel's registry subsystem over the Registry Source Interface (RSI).

loregd is not architecturally special. Any process that implements the RSI contract can serve as a registry source, and the kernel is source-agnostic: it does not know or care that the process answering for a hive keeps its data in SQLite.

What makes loregd special is operational. It is the source that provides the Machine\ and Users\ hives at boot, which puts it on the critical path to a running system — if loregd does not come up, very little else does.

The dev.peios.loregd package installs the daemon, its registryd role provider, and the protected empty state directory. Debugging support is split into dev.peios.loregd-debuginfo and dev.peios.loregd-debugsource; the matching build inputs are published as dev.peios.loregd-source.

peinit owns the compiled-in Phase-1 service definition, so Loregd does not ship a second service seed that could compete with it.

1.1.1 Where loregd sits #

The registry is split across a trust boundary. The kernel side owns the namespace, the layer model, access checks, watches, and transactions; it holds no storage of its own. The source side owns bytes on disk and answers questions about them. loregd is a source, and everything in this manual describes the lower half of that split.

Three consequences run through the whole design:

  • loregd stores; it does not decide. It never resolves layers, never filters results by visibility, and never applies a security descriptor. It returns every layer entry it holds and lets the kernel work out which one wins. The security descriptors in its tables are opaque payload to it.
  • GUIDs come from the kernel. loregd does not mint key identities. It records the GUID it is given, and uses it as the primary key.
  • Sequence numbers come from the kernel. loregd stores them and reports the maximum it holds at registration, but never allocates one.

1.1.2 What this manual covers #

The command line and startup sequence; the SQLite schema backing a hive and the tmpfs-backed store backing volatile keys; the connection model, write serialisation, and how requests are dispatched; and the handling of each RSI operation, including transactions and enumeration ordering.

The kernel half of the registry — the namespace, layers, watches, access control — is chapter 5 of the Peios Kernel TRM, and the RSI wire protocol itself is specified in PSPK. Key schemas belong to the subsystems that own them, and loregd's supervision as a service belongs to the init system's manual.

1.2 Terminology

Peios / Peios technical reference / loregd / Introduction

The registry's own vocabulary — hive, key, value, layer, source, watch, security descriptor, sequence number — is defined by the kernel-side registry documentation and is used here unchanged.

The terms below are specific to loregd.

Hive database. The SQLite database file backing one hive. Each hive registered by loregd has its own file, whose path is given on the command line (§2.1). Referred to in SQL as the main schema.

Volatile database. The tmpfs-backed SQLite WAL database for one hive's volatile keys, attached as volatile. It mirrors the persistent tables, starts empty, and is never reused across daemon instances (§3.3).

Folded name. The case-folded form of a key name, value name, or child name, stored in a _folded column beside the canonical case-preserving name and used for all case-insensitive comparison (§3.4).

Write connection. The single SQLite connection per hive through which every mutation passes. Its uniqueness is what serialises writes (§4.1).

Read pool. The fixed set of connections serving reads that are not part of a transaction, selected round-robin (§4.1).

Snapshot connection. A dedicated connection opened for one read-only transaction, pinning a point-in-time view of the hive database for that transaction's lifetime (§4.3).

Orphan. A key record that no path entry in any layer points at. Orphans are cleaned up at startup (§2.2) and are reported by RSI_DELETE_LAYER as the keys its deletion left unreachable (§5.6).

2.1 Command Line

Peios / Peios technical reference / loregd / Startup

loregd is configured entirely by its argument vector. It takes one or more hive declarations, each naming a hive and the SQLite database file that backs it:

loregd <HiveName>=<DatabasePath> [<HiveName>=<DatabasePath> ...]

For example:

loregd Machine=/var/state/registry/machine.regdb Users=/var/state/registry/users.regdb
loregd Machine=/var/state/registry/machine.regdb Users=/var/state/registry/users.regdb Roles=/var/state/registry/roles.regdb

Each argument is split at its first =, so a database path may itself contain =.

Every declared hive is registered with the kernel at startup (§2.2).

2.1.1 Argument validation #

loregd rejects the invocation and exits with a non-zero status if any of the following hold:

ConditionReason
No hive arguments at allAt least one hive is required.
An argument with no =Not a hive declaration.
An empty hive name or empty pathNeither is meaningful.
A relative database pathPaths are required to be absolute.
A hive name containing \, /, or NULThese are path separators and terminators in the registry namespace.
The hive name CurrentUser, in any caseReserved by the kernel as a per-token alias; no source may claim it.
Two declarations of the same hive nameDuplicates are detected on the folded name, so Machine and MACHINE collide.

Hive-name comparison is case-insensitive throughout — for duplicate detection here, and for routing requests later — but the case as written on the command line is preserved and is what loregd presents to the kernel when it registers.

2.1.2 Unsupported recovery switches #

In the source-checked implementation at 3cb9768 (1 October 2026), --inspector, --recover-from-backup and --dangerously-clear-database are not implemented switches. The argument parser accepts hive declarations only; each of these spellings lacks = and is rejected as a malformed declaration. They are not offline inspection, restoration or clearing commands.

See startup backup and repair limits before treating startup recovery as a way to repair a database. Identify the installed provider and revision before choosing any recovery procedure.

2.1.3 Configuration #

loregd has no configuration file and reads no configuration from the registry. This is deliberate: loregd is the configuration store, and a store that had to read its own configuration in order to start could not start.

Everything about how it behaves comes from three places: the command-line arguments above, the contents of the SQLite databases they name, and compiled-in constants (§4.1).

NOTIFY_SOCKET is the one environment variable loregd consults, and it carries no behavioural setting. When it is set, loregd treats it as a service-manager readiness socket: once the hives are registered and the request loop is about to begin, it connects and sends READY=1. When it is unset, the step is skipped.

loregd writes ordinary progress to standard output and faults to standard error. peinit captures both streams during Phase 1: if loregd fails before readiness, peinit relays the captured diagnostic output to the console; after the log service starts, the pre-eventd buffer carries both streams into eventd. This keeps early-boot failures visible without bypassing the system log.

2.2 Startup Sequence

Peios / Peios technical reference / loregd / Startup

Startup runs to completion before loregd accepts a single request. Any failure in it is fatal: loregd logs the error and exits non-zero rather than serving a hive it could not fully prepare.

2.2.1 1. Parse and validate arguments #

Extract the hive name to database path mapping and apply the validation in §2.1.

2.2.2 2. Open each hive database #

For each declared hive, create the database file's parent directory if it is absent (mode 0700), then open — or create — the SQLite database at that path. Registry hives can contain credentials and security policy, so the fallback directory is private to the service. A packaged system normally receives the same protected directory from the dev.peios.loregd package, but a first boot onto an empty /var/state still works without anything having prepared it.

Four pieces of connection state are established immediately:

  • PRAGMA journal_mode=wal. The result is read back and checked; if the database does not report wal, the open fails. WAL mode is what allows concurrent readers alongside a writer (§4.1), so silently running without it is not acceptable.
  • PRAGMA foreign_keys=ON. Neither schema declares a foreign key, so this constrains nothing.
  • PRAGMA busy_timeout=0 — SQL lock conflicts return busy immediately (§4.4).
  • The volatile database is attached (§3.3).

Each database/sql handle is limited to a single underlying connection.

2.2.3 3. Attach the volatile store #

Each hive gets a fresh tmpfs-backed SQLite database, attached as volatile and put in WAL mode with private caches. Before opening hives, loregd protects and locks /run/loregd, verifies tmpfs, and removes old volatile stores left by a crash (§3.3). It never reuses a prior process's volatile data.

2.2.4 4. Establish the schema #

If the database has no schema_version table, it is new: loregd creates the persistent tables, creates the volatile tables, and stamps the schema version, in one transaction.

If the version is present, it is compared against the version loregd supports. A newer version aborts startup, and so does an older one (§3.1).

2.2.5 5. First-boot root key #

For each hive, look for a key with no parent. If none exists, this is the hive's first boot: loregd generates a random 16-byte GUID for the root key, builds the default hive-root security descriptor, and inserts the root key record.

The default root descriptor grants SYSTEM and Administrators full access to the key, grants Authenticated Users read access, marks all three as container-inheritable, and sets both owner and group to SYSTEM.

2.2.6 6. Crash recovery #

SQLite's own WAL recovery handles any transaction that was uncommitted when the process died; it happens when the database is opened and needs nothing from loregd.

What loregd does on top of that is clean up orphaned keys — key records that no path entry in any layer points at. These can survive a crash between a key's creation and the creation of the path entry naming it. In one transaction, loregd deletes the values belonging to orphaned keys, then their blanket tombstones, then the key records themselves.

The hive root is exempt: it legitimately has no parent and no path entry pointing at it, so orphan detection skips keys whose parent_guid is null.

2.2.7 7. Compute the maximum sequence number #

For each hive, take the maximum sequence across the path entries, values, and blanket tombstones — in both the persistent and the volatile tables — then take the maximum across all hives. That single global figure is what loregd reports at registration, so the kernel can resume allocating sequence numbers above everything already stored.

In practice the volatile tables are empty at this point in startup and contribute nothing.

2.2.8 8. Open the registry device #

Open /dev/pkm_registry. The kernel requires SeTcbPrivilege in the calling thread's token to permit this; loregd performs no check of its own and relies on the kernel to refuse.

Before registration, negotiate RSI lifetime extension version 1 with REG_SRC_NEGOTIATE. Retain the returned source-session identifier for request contexts and lease renewal. A kernel without this extension is incompatible; startup fails before registering hives instead of silently changing lifetime semantics.

2.2.9 9. Register the hives #

Issue REG_SRC_REGISTER with every hive name, its root key GUID, and the global maximum sequence number from step 7. The registration flags are zero: loregd registers global hives only and never private ones.

2.2.10 10. Signal readiness and serve #

Send readiness to NOTIFY_SOCKET if it is set (§2.1), install the termination signal handler (§2.3), and enter the request loop (§4.2).

2.2.11 Backup and repair limits #

The source-checked implementation at 3cb9768 (1 October 2026) does not create an automatic backup at startup. The startup caller opens each hive; hive initialization prepares its database, schema, root, orphan cleanup and reader pool. The orphan cleanup is a transaction deleting unreferenced persistent records, not backup restoration or a database-corruption repair facility.

WAL recovery and RSI_FLUSH checkpointing do not create a backup copy. Separately, loregd supports read-only RSI snapshot transactions used by higher-level backup operations. A transaction snapshot is not an automatically saved startup backup. See the LCS backup contract for that separate operation; this startup behavior does not establish an end-to-end offline repair or restore procedure.

Verify the existence and coverage of a recovery copy rather than assuming startup made one. The command-line limits also rule out the previously described loregd repair switches for this implementation.

2.3 Exit and Shutdown

Peios / Peios technical reference / loregd / Startup

2.3.1 What ends the process #

loregd exits when any of the following happens:

  • The kernel closes the registry device. Reading from /dev/pkm_registry returns end-of-file, the request loop returns, and loregd shuts down cleanly with status 0. This is the normal path when the registry subsystem goes away.
  • A termination signal arrives. SIGTERM and SIGINT are trapped. The handler closes the device, which unblocks the read loop and produces the same clean shutdown as above. This is how the service manager stops loregd.
  • A request cannot be framed. If a message read from the device cannot be parsed as an RSI request, loregd treats it as unrecoverable and exits non-zero.
  • Startup fails. Any error in §2.2 is fatal.

On shutdown, in-flight requests are drained before the process exits (§4.2), then outstanding transactions are rolled back, every connection is closed, and the per-hive volatile directories are removed before the runtime lock is released.

2.3.2 What does not end the process #

A storage failure during request handling does not terminate loregd. Errors from SQLite while serving a request — including I/O errors — are converted into an RSI_STORAGE_ERROR response and the daemon carries on serving. There is no corruption detector and no disk-full detector that takes the process down; a database that has become unreadable will produce a stream of storage errors rather than an exit.

Note

This is worth knowing when diagnosing a system whose registry has started failing: the symptom is failing operations, not a dead daemon, and loregd will still be running and still registered.

2.3.3 What happens to the data #

Persistent data is durable at the point each transaction commits; SQLite finalises any outstanding WAL state as the connections close.

Volatile data is never recovered after process exit. Clean shutdown removes the tmpfs stores; startup removes crash residue and creates new stores before registering the hives (§3.3).

When the kernel observes the source disconnect, it marks every hive loregd served as unavailable.

3.1 Schema Version

Peios / Peios technical reference / loregd / Storage

Every hive database carries a schema version in a single-row table :

CREATE TABLE schema_version (
    version INTEGER NOT NULL
);

The current version is 1 — the only version that has existed.

loregd checks the version as it opens each database (§2.2, step 4) and takes one of three paths:

StateBehaviour
No schema_version tableThe database is new. loregd creates the persistent tables, the volatile tables, and inserts version 1, all in one transaction.
Version equals 1Normal startup.
Version greater than 1Startup fails. The database was written by a newer loregd, and proceeding risks corrupting it by writing through an older understanding of its layout.
Version less than 1Startup fails.

There are no migrations. loregd carries no migration table, no migration step list, and no upgrade path; an older database is reported as requiring migration and startup stops there. Since 1 is the only version ever assigned, this does not arise in practice — but a second version cannot be stamped without building the migration machinery first.

Two details of the check are worth knowing. The schema_version table has no primary key, uniqueness constraint, or check constraint, so a second row is not detected: the first row read wins.

And because every table is created with IF NOT EXISTS, a database holding the data tables but no schema_version table is stamped as version 1 without any validation that its contents match that layout.

3.2 Persistent Tables

Peios / Peios technical reference / loregd / Storage

Each hive is one SQLite database, and every hive database has the same four data tables. loregd creates them on first boot (§2.2, step 4).

The tables hold what the kernel gives loregd and nothing derived from it. Security descriptors are stored as opaque blobs, GUIDs and sequence numbers are assigned by the kernel, and no table records a resolved or filtered view of anything.

3.2.1 keys #

CREATE TABLE keys (
    guid           BLOB NOT NULL PRIMARY KEY,
    name           TEXT NOT NULL,
    name_folded    TEXT NOT NULL,
    parent_guid    BLOB,
    sd             BLOB NOT NULL,
    volatile       INTEGER NOT NULL DEFAULT 0,
    symlink        INTEGER NOT NULL DEFAULT 0,
    last_write_time INTEGER NOT NULL
);
ColumnMeaning
guidThe 16-byte key GUID assigned by the kernel. Primary key.
nameThe key's own name component, with case preserved as written.
name_foldedThe folded form of name (§3.4), used for case-insensitive lookup.
parent_guidThe parent key's GUID; null for the hive root, which is how the root is identified.
sdThe security descriptor, in binary self-relative form. Opaque to loregd.
volatile1 for a volatile key, 0 for a persistent one. In this table it is always 0 — volatile keys live in the volatile database (§3.3).
symlink1 if the key is a symbolic link.
last_write_timeUnix nanoseconds.

3.2.2 path_entries #

CREATE TABLE path_entries (
    parent_guid       BLOB NOT NULL,
    child_name        TEXT NOT NULL,
    child_name_folded TEXT NOT NULL,
    layer             TEXT NOT NULL,
    target_type       INTEGER NOT NULL,
    target_guid       BLOB,
    sequence          INTEGER NOT NULL,
    PRIMARY KEY (parent_guid, child_name_folded, layer)
);

CREATE INDEX idx_path_entries_target
    ON path_entries (target_guid)
    WHERE target_type = 0;

A path entry is one layer's opinion about one child name under one parent. Several layers may hold entries for the same name; resolving between them is the kernel's job, not loregd's.

ColumnMeaning
parent_guidThe parent key's GUID.
child_nameThe child name with case preserved.
child_name_foldedThe folded form, which is what the primary key uses — so a name collides case-insensitively within a layer.
layerThe layer name. Compared as binary, so layer names are case-sensitive, unlike key names.
target_type0 for a GUID entry (the key exists in this layer), 1 for HIDDEN (a tombstone masking lower layers).
target_guidThe target key's GUID when target_type is 0; null for HIDDEN.
sequenceThe kernel-assigned sequence number.

The partial index on target_guid covers only non-HIDDEN rows. It is what makes the reverse lookup — which path entries point at this key — cheap, and that reverse lookup is what orphan detection (§2.2, step 6) and RSI_DROP_KEY need.

3.2.3 values #

CREATE TABLE [values] (
    key_guid       BLOB NOT NULL,
    name           TEXT NOT NULL,
    name_folded    TEXT NOT NULL,
    layer          TEXT NOT NULL,
    type           INTEGER NOT NULL,
    data           BLOB,
    sequence       INTEGER NOT NULL,
    PRIMARY KEY (key_guid, name_folded, layer)
);
ColumnMeaning
key_guidThe key this value belongs to.
nameThe value name, case preserved. The empty string is the key's default value.
name_foldedThe folded form; also the empty string for the default value.
layerThe layer this value entry belongs to.
typeThe registry value type — REG_SZ is 1, REG_DWORD is 4, and so on. REG_TOMBSTONE (0xFFFF) marks a per-value tombstone.
dataThe value payload; null for a tombstone.
sequenceThe kernel-assigned sequence number.

values is a reserved word in SQL, so every reference to this table is quoted — [values], or main.[values] and volatile.[values] when the schema is named explicitly. Unquoted, it is a syntax error.

3.2.4 blanket_tombstones #

CREATE TABLE blanket_tombstones (
    key_guid       BLOB NOT NULL,
    layer          TEXT NOT NULL,
    sequence       INTEGER NOT NULL,
    PRIMARY KEY (key_guid, layer)
);

A blanket tombstone hides every value a key holds in the layers beneath it, rather than naming one value the way a REG_TOMBSTONE entry does. One row per key per layer.

3.3 The Volatile Store

Peios / Peios technical reference / loregd / Storage

Volatile keys never reach the persistent hive file. Each hive attaches a separate SQLite database as volatile, using WAL and private connection caches. Its files live on tmpfs beneath /run/loregd/hive-<random>/.

The runtime directory has a protected, inheritable SYSTEM-only KACS DACL. Startup verifies that it is on tmpfs and refuses symlinks. A stable .lock file prevents two daemon instances from cleaning up each other's stores.

3.3.1 A mirror of the persistent schema #

The volatile database carries the same four tables as the persistent one — keys, path_entries, [values], and blanket_tombstones — with identical columns, identical primary keys, and the same partial idx_path_entries_target index on non-HIDDEN path entries. Column meanings are exactly those in §3.2.

There is one deliberate difference. In volatile.keys the volatile column defaults to 1 rather than 0, and loregd writes 1 into it. Every record in this database is volatile by definition, and the column carries that fact back out in responses without a second lookup.

Because the two schemas are structurally identical, a query that needs both stores is a UNION ALL across them rather than two queries merged in application code:

SELECT 1 FROM main.keys WHERE guid = ?
UNION ALL
SELECT 1 FROM volatile.keys WHERE guid = ?
LIMIT 1

3.3.2 Why this shape matters #

Making the volatile store a SQLite database attached to the same connection buys transactional behaviour for free. A SQLite transaction spans every attached schema, so a transaction that mutates both persistent and volatile data commits or rolls back as one unit, with no separate mechanism to keep the two halves consistent.

Volatile writes inside a transaction are invisible to other readers until commit and disappear on rollback for the same reason persistent ones do — see §4.3.

3.3.3 Lifetime #

Every open hive gets a fresh random directory. Read, write and snapshot connections attach the same database file for that hive. Different hive instances never share a store merely because their names match.

Clean shutdown closes the connections and removes the hive directories. After a crash, files may remain in tmpfs until the next start or reboot. The next daemon acquires the runtime lock and removes that residue before creating fresh stores; it never reads or recovers old volatile data. The .lock file remains; its kernel lock ends when the process exits. Every volatile table is empty on startup, including a restart within the same boot.

3.3.4 Which store an operation uses #

For an operation naming a single key, the key's own storage decides: loregd looks the GUID up in main.keys and volatile.keys, and whichever holds it determines where the reads and writes go.

RSI_CREATE_KEY is the exception, because the key does not exist yet — the volatile flag in the request decides which database receives it.

Two operations are not scoped to one store at all. RSI_LOOKUP and RSI_ENUM_CHILDREN consult both and combine the results, because a persistent parent may legitimately have volatile children. The reverse does not arise: a persistent child beneath a volatile parent is forbidden by the kernel's data model, so a volatile key's whole subtree is volatile.

3.4 Case Folding

Peios / Peios technical reference / loregd / Storage

Key names and value names are case-insensitive but case-preserving. loregd implements this by storing both forms: the name as written, and a folded form alongside it in a _folded column.

The folded form is computed once, when the name is written, and it is the folded column that appears in every WHERE clause and every primary key. Lookups are therefore plain binary comparisons :

SELECT * FROM path_entries
WHERE parent_guid = ? AND child_name_folded = ?

This is why no custom SQLite collation is registered anywhere in loregd — the case-insensitivity has already happened by the time SQLite sees the query.

The canonical name is what comes back in responses, so callers see the case they originally supplied while storage compares the folded form.

Hive names are folded too, though not stored : routing a request to a hive and rejecting duplicate hive declarations (§2.1) both compare folded names.

Layer names are not folded. They are compared as binary, so layer names are case-sensitive.

3.4.1 How the folded form is computed #

The folded form is Unicode Simple Case Folding: each codepoint is replaced by its status C or S mapping from CaseFolding.txt, Unicode 16.0.0, or left alone when it has none. This is the same rule the kernel applies when it compares layer names and every other name in LCS, and loregd must produce byte-identical results, because a name the two fold differently is a key that exists for one side and not the other.

Identity is guaranteed by construction rather than by care. loregd does not derive the fold from the Go standard library, whose case tables implement lowercasing (a different operation) at a different Unicode version. Its table is generated by the kernel's own generator, pkm/tools/lcs/generate_casefold_table.py, from the same CaseFolding.txt that produces the kernel's table; the generated Go file records the source version and checksum, and the lookup mirrors the kernel's binary search over the same ranges and pairs. A Unicode version bump regenerates both outputs together.

Note

Lowercasing is not case folding, and the difference is not cosmetic. Folding maps 1,484 codepoints; against Go's ToLower the two disagree on 222 of them. The Cherokee blocks fold towards uppercase, so lowercasing sends all 172 of them the wrong way; Greek final sigma ς and the symbol variants ϐ ϑ ϕ ϖ ϰ ϱ ϵ fold to their base letters but lowercase to themselves; the Cyrillic historic letters at U+1C80–U+1C89 and the Garay block, new in 16.0, have no lowercase mapping at all. With lowercasing, a key stored as Σ is not found by a lookup for ς. loregd used lowercasing plus three corrections until September 2026; folded columns written by that implementation differ from the current rule for names containing any of those 222 codepoints.

4.1 Connections

Peios / Peios technical reference / loregd / Concurrency

Each hive holds a small, fixed set of SQLite connections, and which one an operation runs on decides both its isolation and what it can block on.

ConnectionCountUsed for
WriteOne per hiveEvery mutating operation, and every read inside a bound read-write transaction.
Read poolmin(NumCPU, 16), at least 1Reads outside a transaction. Selected round-robin.
SnapshotOne per active read-only transactionReads inside a read-only transaction. Created on demand, closed when the transaction ends.

The pool size is compiled in and cannot be configured — loregd reads no configuration (§2.1).

4.1.1 One connection per handle #

Each Go database/sql handle has one physical connection. The write handle is reserved by a bound read-write transaction. Other mutations return busy rather than waiting for that reservation; per-hive gates coordinate admission and publication (§4.4).

It also means the snapshot connections are genuinely separate handles rather than borrowed pool entries, which is what keeps a long-running read-only transaction from consuming a pool slot.

4.1.2 What every connection carries #

Connection state is established on every physical connection, including one that replaces a connection discarded after cancellation:

  • journal_mode=wal on both databases, verified after being set.
  • foreign_keys=ON.
  • busy_timeout=0 — SQL lock conflicts return busy immediately (§4.4).
  • The hive's tmpfs database attached as volatile (§3.3).

Both stores have private-cache WAL readers. The volatile tables are created on the write connection; other connections attach the same file. A read-only transaction pins a snapshot of both stores before a commit can publish.

4.1.3 Which connection an operation uses #

Nine mutating operations are routed to the write connection through the transaction-aware write path: RSI_CREATE_KEY, RSI_WRITE_KEY, RSI_DROP_KEY, RSI_CREATE_ENTRY, RSI_HIDE_ENTRY, RSI_DELETE_ENTRY, RSI_SET_VALUE, RSI_DELETE_VALUE_ENTRY, and RSI_SET_BLANKET_TOMBSTONE.

Four read operations are routed to the read pool, or to the transaction's connection when one is bound: RSI_LOOKUP, RSI_ENUM_CHILDREN, RSI_READ_KEY, and RSI_QUERY_VALUES.

RSI_DELETE_LAYER and RSI_FLUSH take the hive's write connection directly rather than through the transaction-aware path, and open and commit work of their own. Neither can join a caller's transaction: RSI_FLUSH checkpoints a hive, and RSI_DELETE_LAYER spans every registered hive while a transaction binds to exactly one.

They do still consult the request's transaction id, for the two answers that do not require joining. A read-only transaction is declined with RSI_INVALID, as it is on every other mutating path. And because each hive allows a single write connection, RSI_DELETE_LAYER declines with RSI_TXN_BUSY while any transaction holds a pinned write connection rather than blocking on it forever — the same answer RSI_FLUSH gives for its own hive.

4.2 Request Dispatch

Peios / Peios technical reference / loregd / Concurrency

RSI requests arrive multiplexed on the /dev/pkm_registry file descriptor. Each carries a request id and a transaction id in its header.

loregd reads messages into a single 16 MiB buffer, copies each one out, and hands it to a new goroutine — one per request, with no cap on how many may be in flight.

Responses are serialised by a mutex, because one write to the device must correspond to exactly one response.

On shutdown the in-flight goroutines are drained before the process exits.

4.2.1 Identifying the target hive #

Most operations name a key GUID (or, for lookups and enumerations, a parent GUID), and loregd must decide which hive owns it.

A guidCache maps GUID to hive. It is seeded at startup with every hive's root GUID, and maintained as keys come and go:

  • RSI_CREATE_KEY stores the new GUID immediately, before the transaction that created it commits, and registers an abort hook to evict it if that transaction rolls back. The immediate store is necessary because the cache-miss probe reads through the pool, which cannot see uncommitted rows.
  • RSI_DROP_KEY evicts immediately outside a transaction, or through a commit hook inside one.
  • RSI_DELETE_LAYER evicts the GUIDs its deletion orphaned.

On a miss, loregd probes each hive in turn:

SELECT 1 FROM main.keys WHERE guid = ?
UNION ALL
SELECT 1 FROM volatile.keys WHERE guid = ?
LIMIT 1

Only hits are cached, so a GUID that exists nowhere is re-probed against every hive on every request that names it. The cache has no size bound; it shrinks only through the three eviction paths above.

A GUID that resolves to no hive produces RSI_NOT_FOUND for most operations. RSI_DROP_KEY and the entry deletions treat it differently — see §5.3 and §5.4.

4.2.2 Operations that resolve differently #

RSI_FLUSH carries a hive name rather than a GUID and resolves it by folded name (§3.4), so the match is case-insensitive.

RSI_DELETE_LAYER does not resolve a hive at all: it applies to every registered hive and concatenates the orphan sets (§5.6).

4.3 Transactions

Peios / Peios technical reference / loregd / Concurrency

Transaction identifiers are allocated by the kernel and carried on every request. loregd binds them to connections lazily — RSI_BEGIN_TRANSACTION does no SQLite work at all.

4.3.1 Beginning #

RSI_BEGIN_TRANSACTION carries the transaction id and a mode: RSI_TXN_READ_WRITE (0) or RSI_TXN_READ_ONLY (1). loregd records the id as pending in the requested mode and returns RSI_OK immediately. No connection is taken and no SQLite transaction is opened.

loregd supports both modes — its SQLite backing provides atomic read-write commits and stable read-only snapshots — so it never returns RSI_TXN_NOT_SUPPORTED.

Re-using a transaction id that is already active returns RSI_INVALID. A missing or truncated mode field, or a mode other than 0 or 1, returns RSI_INVALID without recording a transaction. The id remains available for a subsequent well-formed request. Callers must send the complete u64 id and u32 mode that PSPK §4.4 requires; a request carrying only the id is rejected, not treated as read-write.

4.3.2 Read-write transactions #

The transaction binds to a hive on its first mutating operation: loregd identifies the hive from the operation's GUID, acquires that hive's write connection, issues BEGIN IMMEDIATE, and records the binding. If SQLite reports SQLITE_BUSY at that point, the operation returns RSI_TXN_BUSY.

Once bound, every subsequent operation with that transaction id — reads included — runs on the same connection. That is what provides read-your-own-writes: uncommitted rows are visible to the transaction because it is the connection that wrote them. Reads issued before the transaction binds go to the read pool instead, since there is nothing uncommitted to see.

One SQLite transaction spans both databases. A per-hive publication gate excludes readers while a commit publishes, so readers cannot observe one store before the commit and the other after it. Non-transactional mutations use the same gate for their whole operation. The gate is released between transaction requests, allowing ordinary readers to see committed data while a write transaction remains open.

SQLite WAL does not provide crash-atomic commit across attached database files. After a crash, the persistent hive is recovered by SQLite and the volatile store is discarded, never replayed (§3.3).

An operation whose GUID belongs to a different hive than the transaction is bound to is rejected with RSI_STORAGE_ERROR. The kernel enforces hive-scoping before requests reach loregd, so this is a backstop.

4.3.3 Read-only transactions #

A read-only transaction binds on its first read. loregd identifies the hive, opens a dedicated connection — deliberately not one from the read pool, so a long-lived snapshot cannot starve ordinary reads — and issues BEGIN DEFERRED. WAL fixes the snapshot at that first read, and every later read with the same transaction id reuses the connection and observes the same point in time.

Before the first request proceeds, loregd reads both schemas while holding the hive gate, pinning both WAL snapshots at the same published state. Later reads, including the first access to a volatile key, retain that state. The snapshot does not block volatile writes.

A mutating operation carrying a read-only transaction id is rejected with RSI_INVALID before any state changes. The nine operations that route through the write path get this from the routing itself; RSI_DELETE_LAYER and RSI_FLUSH cannot route that way and ask directly, for the same answer (§4.1).

4.3.4 Committing and aborting #

RSI_COMMIT_TRANSACTION issues COMMIT on the bound connection and returns RSI_OK. If the commit fails, the transaction is left open so the caller may retry or abort: a busy or locked failure returns RSI_TXN_BUSY, anything else RSI_STORAGE_ERROR. Committing an unknown transaction id returns RSI_STORAGE_ERROR.

Committing a read-only transaction releases the snapshot and returns RSI_OK.

RSI_ABORT_TRANSACTION issues ROLLBACK, closes the connection, releases any snapshot, runs the transaction's abort hooks, and always returns RSI_OK — including for a transaction id it has never seen. A connection with an uncertain rollback is discarded rather than returned to the pool. This is how the kernel releases a read-only snapshot after a REG_IOC_BACKUP finishes or fails.

LCS sends aborts when a client transaction expires or its last handle closes. loregd also rolls back every remaining transaction during shutdown. A renewable cleanup lease also reclaims state when an abort is lost. Its authority comes from the live kernel owner, for both read-write transactions and read-only snapshots; it is not an inactivity timeout (§4.4).

If a request interrupts a bound SQLite transaction, loregd marks that ID failed. Reads, writes and commits on it return a storage error until LCS aborts it; it is never silently rebound. This handles SQLite's possible automatic rollback after interruption.

4.4 Waiting and Contention

Peios / Peios technical reference / loregd / Concurrency

4.4.1 Request deadlines #

Each RSI operation uses the absolute CLOCK_MONOTONIC deadline supplied by LCS through REG_SRC_REQUEST_CONTEXT. The budget includes kernel admission, queue time, connection-pool waits, publication-gate waits and SQL statements; receiving the request does not restart it. A contended wait that expires returns RSI_TXN_BUSY; successful operations retain their success status. SQLite's busy timeout is zero because its native busy wait can outlast a canceled Go context. Busy classification uses SQLite and Go error codes.

The lifetime extension is negotiated before hive registration. loregd requires version 1 and fails startup on a kernel without it. The 25-second local budget is only a fallback for direct, unnegotiated test clients.

4.4.2 A transaction holding the writer #

A competing mutation returns RSI_TXN_BUSY immediately while a read-write transaction owns the hive's write connection. This includes non-transactional key drops, conditional value writes, flushes and layer deletion. Admission and binding are coordinated under the hive gate, so the check cannot race a new binding. A read-only snapshot is not counted as a writer.

Both stores use WAL with private caches. Ordinary reads see committed data while a writer remains open; backup snapshots do not hold volatile table locks that block writers. A short per-hive gate excludes reads from commit publication and non-transactional mutations, not from an entire transaction's lifetime. Different hives have separate gates.

4.4.3 Cancellation and cleanup #

Operations on the same transaction are serialized, including commit and abort. Rollback uses a fresh cleanup context, and an uncertain connection is discarded rather than returned to the pool with a live transaction. Every replacement physical connection receives its WAL, foreign-key and volatile-attachment setup again.

SQLite may automatically roll back an interrupted write. Such a bound transaction is marked failed and subsequent use returns a storage error; LCS must abort it. It is never silently rebound or allowed to autocommit.

LCS owns transaction lifetime. A live transaction file object (including all duplicated handles) or an in-kernel operation owns the shared kernel transaction record. A source can renew its five-second cleanup lease only while that owner and source session remain valid. SQL activity neither creates ownership nor extends a transaction's lifetime deadline. An internal operation may have no separate lifetime limit; its live operation scope is still required.

A separate control loop renews leases with three seconds or less remaining, checks expiration every 100 milliseconds, and reclaims expired transactions. Control ioctls consume no ordinary RSI request slots. Final owner release, abort, timeout or a lost source session prevents renewal even if an abort message cannot be delivered. Expired transactions reject new operations; late renewals cannot revive them. Cleanup serializes on the transaction's operation gate and waits for an already-admitted commit to resolve. A request timeout is not evidence that a commit failed. The request deadline gates commit admission; once admitted, SQL COMMIT runs without caller cancellation and its actual result is reported, including through normal late-response recovery. Shutdown drains requests and cleanup before closing databases.

5.1 Store Routing

Peios / Peios technical reference / loregd / Request Handling

Persistent data lives in the hive database's main schema; volatile data lives in the attached volatile schema (§3.3). Most operations act on one of the two, and loregd has to decide which before it can run any SQL.

5.1.1 Operations naming one key #

For a write that names an existing key, the schema containing the key row selects the store. loregd first probes main.keys by GUID, then probes volatile.keys only if the first query found no row. A database error is returned as a storage error, not treated as absence. If an externally modified database contains the GUID in both stores, the existing main-first lookup precedence applies; normal key creation rejects such duplicates.

A GUID present in neither produces RSI_NOT_FOUND for most operations.

Routing follows the source table, including when the stored volatile column contradicts it. A row in main.keys carrying volatile = 1 still routes writes to main; a row in volatile.keys carrying 0 still routes writes to volatile. An inconsistent flag must not redirect writes to the other store. Rows loregd creates carry the consistent flag for their store. This routing rule does not repair external metadata changes: read-key and lookup metadata continue to report the stored column value.

RSI_CREATE_KEY cannot consult a key that does not exist yet, so it routes on the volatile flag carried in the request instead.

RSI_CREATE_ENTRY routes to the store containing the child key the entry points at. When that child GUID is present in neither store, the entry is written to the persistent table.

RSI_HIDE_ENTRY routes on the parent key, since a HIDDEN entry belongs to the parent's child list and a volatile parent's whole subtree is volatile.

5.1.2 Operations spanning both stores #

RSI_LOOKUP, RSI_ENUM_CHILDREN, RSI_READ_KEY and RSI_QUERY_VALUES are not scoped to one store: a persistent parent may have volatile children, and a persistent key may have volatile-store rows beneath it. Each issues a single UNION ALL statement over the two schemas rather than querying them separately, so the merge happens inside SQLite.

Nothing de-duplicates across the two stores. The primary keys that make (parent_guid, child_name_folded, layer) unique apply per schema, so if the same triple exists in both, both rows appear in the response. The same holds for value entries keyed on (key_guid, name_folded, layer).

The deletions — RSI_DELETE_ENTRY, RSI_DELETE_VALUE_ENTRY and RSI_DROP_KEY — do not route at all. They delete from both schemas unconditionally.

5.2 Response Ordering

Peios / Peios technical reference / loregd / Request Handling

The queries backing enumerations and lookups carry no ORDER BY, and a UNION ALL across the two stores yields rows in whatever order SQLite produces them. That order is not stable across calls.

The kernel walks enumeration results by dense index across repeated calls, so an unstable order would make that walk duplicate or drop entries. loregd therefore sorts every affected array into a canonical order before encoding a response:

ResponseSorted by
RSI_LOOKUP path entrieslayer, then sequence
RSI_ENUM_CHILDREN childrenfolded child name
RSI_ENUM_CHILDREN per-child entrieslayer, then sequence
RSI_QUERY_VALUES value entriesfolded value name, then layer, then sequence
RSI_QUERY_VALUES blanket tombstonesfolded layer name, then sequence
RSI_DELETE_LAYER orphan GUIDsascending GUID, compared bytewise
Key-metadata blocks, in any responseascending GUID, compared bytewise

This ordering is a wire-stability guarantee only. It has no bearing on layer resolution, which is order-independent — the kernel selects a maximum, not a first match.

5.2.1 Arrays assembled outside one query #

Two of those arrays are not a single query's result set. They are assembled first and sorted afterwards:

  • The blanket-tombstone array in an RSI_QUERY_VALUES response comes from its own UNION ALL, separate from the value entries it travels beside, and takes the same order they do.
  • The orphan-GUID array in an RSI_DELETE_LAYER response is the concatenation of every registered hive's orphan set. That walk ranges a Go map, whose iteration order is randomised, so the array is sorted bytewise before it is encoded.

5.2.2 Child display names #

RSI_ENUM_CHILDREN groups rows by child_name_folded and emits one child block per folded name, carrying a display name taken from the child_name column.

Where two rows share a folded name but differ in stored case — Foo in one store and FOO in the other, say — the display name emitted is the lower of the two bytewise, which is a property of the set rather than of the order the union produced. Both the order of children and the case of the name reported for each are therefore stable across calls.

5.3 Key Operations

Peios / Peios technical reference / loregd / Request Handling

5.3.1 RSI_CREATE_KEY #

The request's volatile flag selects the target table (§5.1). For a persistent key:

INSERT INTO keys
    (guid, name, name_folded, parent_guid, sd, volatile, symlink,
     last_write_time)
VALUES (?, ?, fold(?), ?, ?, 0, ?, ?)

last_write_time is not carried in the request. loregd sets it to the current wall-clock time in Unix nanoseconds at insertion.

Uniqueness comes from the target table's primary key on guid, surfaced as RSI_ALREADY_EXISTS. That key binds one schema, so loregd also asks the other store whether it holds the GUID before inserting, and answers RSI_ALREADY_EXISTS if it does. Without that check a GUID could come to exist in both stores, where metadata reads would resolve it to the main row — the reading query takes the first row of a UNION ALL that puts main first — leaving the volatile row unreachable while it still occupied its GUID.

The new GUID is added to the hive cache immediately, before the enclosing transaction commits, with an abort hook to remove it if that transaction rolls back (§4.2).

An unresolvable parent GUID returns RSI_NOT_FOUND, after a fallback check of the registered hives' root GUIDs.

5.3.2 RSI_READ_KEY #

SELECT name, parent_guid, sd, volatile, symlink, last_write_time
FROM main.keys WHERE guid = ?
UNION ALL
SELECT name, parent_guid, sd, volatile, symlink, last_write_time
FROM volatile.keys WHERE guid = ?
LIMIT 1

RSI_NOT_FOUND if the GUID is in neither store, and likewise if it resolves to no hive. The volatile field in the response is the stored column value.

5.3.3 RSI_WRITE_KEY #

Updates the two mutable fields of a key, selected by a field mask:

BitValueField
00x01sd
10x02last_write_time

Valid masks are therefore 0x00, 0x01, 0x02 and 0x03. Any other bit set returns RSI_INVALID — it indicates an attempt to modify an immutable field.

loregd builds one UPDATE from the mask, setting only the named fields:

-- mask 0x03
UPDATE keys SET sd = ?, last_write_time = ? WHERE guid = ?

A mask of 0x00 names no fields and acts as an existence check, returning RSI_OK or RSI_NOT_FOUND. An update that matches no row also returns RSI_NOT_FOUND.

Outside a transaction the update is a single auto-committed statement. Inside one it runs on the transaction's connection.

5.3.4 RSI_DROP_KEY #

Purges every trace of a GUID from both stores — four tables in each schema:

DELETE FROM keys               WHERE guid = ?;
DELETE FROM path_entries       WHERE target_guid = ?;
DELETE FROM [values]           WHERE key_guid = ?;
DELETE FROM blanket_tombstones WHERE key_guid = ?;

Outside a transaction, the eight statements are wrapped in a BEGIN IMMEDIATE transaction of their own so the purge is atomic. Inside one, they run on the transaction's connection.

Dropping a GUID that does not exist returns RSI_OK; so does one that resolves to no hive. The operation is idempotent. The GUID is evicted from the hive cache (§4.2).

5.4 Path Entry Operations

Peios / Peios technical reference / loregd / Request Handling

5.4.1 RSI_LOOKUP #

Returns every layer's entry for one child name under one parent, together with metadata for the keys those entries point at.

SELECT layer, target_type, target_guid, sequence
FROM main.path_entries
WHERE parent_guid = ? AND child_name_folded = ?
UNION ALL
SELECT layer, target_type, target_guid, sequence
FROM volatile.path_entries
WHERE parent_guid = ? AND child_name_folded = ?

HIDDEN entries are returned as entries but contribute no metadata GUID. For each distinct non-HIDDEN target_guid, loregd fetches the key's metadata — one query per GUID — and emits the blocks in ascending GUID order (§5.2).

loregd does no layer filtering and no resolution: every entry it holds is returned, and choosing between them is the kernel's job.

An unresolvable parent GUID returns RSI_NOT_FOUND.

If a path entry names a target_guid for which no key record exists, loregd drops that entry from the response and returns RSI_OK, so the child reads as absent. This is reachable in ordinary operation: the kernel issues RSI_CREATE_ENTRY before RSI_CREATE_KEY, so a lookup landing between the two sees an entry whose key has not yet been written, and failing the request would make a routine race fatal to the caller.

5.4.2 RSI_CREATE_ENTRY #

INSERT INTO path_entries
    (parent_guid, child_name, child_name_folded, layer,
     target_type, target_guid, sequence)
VALUES (?, ?, fold(?), ?, 0, ?, ?)

The target table follows the store containing the child key (§5.1). A child GUID in neither store lands in the persistent table.

RSI_ALREADY_EXISTS comes from the target table's primary key on (parent_guid, child_name_folded, layer). That key binds one schema, so loregd also asks the other store for the same triple before inserting, and answers RSI_ALREADY_EXISTS if it finds one. Nothing de-duplicates on read, so a triple in both stores would put two entries claiming the same name in the same layer inside one child block (§5.1).

An unresolvable parent GUID returns RSI_NOT_FOUND.

5.4.3 RSI_HIDE_ENTRY #

Writes a tombstone that masks the same name in lower layers:

INSERT OR REPLACE INTO path_entries
    (parent_guid, child_name, child_name_folded, layer,
     target_type, target_guid, sequence)
VALUES (?, ?, fold(?), ?, 1, NULL, ?)

target_type is 1 and target_guid is null. The target table follows the store containing the parent key, because a volatile parent's entire subtree is volatile. A parent GUID in neither store returns RSI_NOT_FOUND.

5.4.4 RSI_DELETE_ENTRY #

Removes one layer's entry for one name, from both stores:

DELETE FROM path_entries
WHERE parent_guid = ? AND child_name_folded = ? AND layer = ?

No rows-affected check is made, so deleting an entry that is not there succeeds. A parent GUID that resolves to no hive returns RSI_NOT_FOUND rather than succeeding.

5.4.5 RSI_ENUM_CHILDREN #

Returns every layer's entry for every child under a parent:

SELECT child_name, child_name_folded, layer, target_type,
       target_guid, sequence
FROM main.path_entries WHERE parent_guid = ?
UNION ALL
SELECT child_name, child_name_folded, layer, target_type,
       target_guid, sequence
FROM volatile.path_entries WHERE parent_guid = ?

Rows are grouped by folded child name into one block per child, each carrying that child's per-layer entries. Metadata for the distinct non-HIDDEN target GUIDs is fetched and emitted exactly as for RSI_LOOKUP, including its treatment of an entry whose key record does not yet exist: the entry is dropped, and a child left holding no entries at all is dropped with it, so the child reads as absent rather than failing the enumeration. The metadata block for a dropped entry is omitted with it — the kernel rejects a GUID-typed entry carrying no metadata, and equally a metadata block no entry references.

Ordering, and the treatment of two rows whose folded names match but whose stored case differs, are covered in §5.2.

5.5 Value Operations

Peios / Peios technical reference / loregd / Request Handling

5.5.1 RSI_QUERY_VALUES #

Returns every layer's entry for one value, or for all of a key's values when the request sets the query-all flag:

-- single value
SELECT name, layer, type, data, sequence
FROM main.[values] WHERE key_guid = ? AND name_folded = ?
UNION ALL
SELECT name, layer, type, data, sequence
FROM volatile.[values] WHERE key_guid = ? AND name_folded = ?

-- query all
SELECT name, layer, type, data, sequence
FROM main.[values] WHERE key_guid = ?
UNION ALL
SELECT name, layer, type, data, sequence
FROM volatile.[values] WHERE key_guid = ?

The response also carries the key's blanket-tombstone state:

SELECT layer, sequence
FROM main.blanket_tombstones WHERE key_guid = ?
UNION ALL
SELECT layer, sequence
FROM volatile.blanket_tombstones WHERE key_guid = ?

Value entries and blanket tombstones are both sorted (§5.2).

An unresolvable GUID returns RSI_NOT_FOUND. An existing key with no values returns RSI_OK with empty arrays.

5.5.2 RSI_SET_VALUE #

The store containing the key selects the target table (§5.1). A key GUID in neither store returns RSI_NOT_FOUND.

INSERT OR REPLACE INTO [values]
    (key_guid, name, name_folded, layer, type, data, sequence)
VALUES (?, ?, fold(?), ?, ?, ?, ?)

5.5.2.1 Conditional writes #

When the request carries a non-zero expected_sequence, the write is a compare-and-swap. loregd reads the current entry's sequence and writes only if it matches:

SELECT sequence FROM [values]
WHERE key_guid = ? AND name_folded = ? AND layer = ?

If the row is absent, or its sequence differs, the operation returns RSI_CAS_FAILED and writes nothing.

Outside a transaction, the check and the write are wrapped in their own BEGIN IMMEDIATE transaction so no other writer can interleave. If that transaction cannot begin because the database is busy, the operation returns RSI_TXN_BUSY. Inside a transaction, the caller's transaction already provides the isolation.

5.5.3 RSI_DELETE_VALUE_ENTRY #

Removes one layer's entry for one value, from both stores:

DELETE FROM [values]
WHERE key_guid = ? AND name_folded = ? AND layer = ?

No rows-affected check, so deleting an absent entry succeeds. A GUID that resolves to no hive returns RSI_NOT_FOUND.

5.5.4 RSI_SET_BLANKET_TOMBSTONE #

Sets or clears the tombstone that masks every value a key holds in lower layers. The store containing the key selects the target table (§5.1). A GUID in neither returns RSI_NOT_FOUND.

-- set
INSERT OR REPLACE INTO blanket_tombstones (key_guid, layer, sequence)
VALUES (?, ?, ?)

-- clear
DELETE FROM blanket_tombstones WHERE key_guid = ? AND layer = ?

5.6 Layer and Maintenance Operations

Peios / Peios technical reference / loregd / Request Handling

Both operations in this section consult the request's transaction id before doing anything — a read-only transaction may not mutate, and an unknown id is refused (§4.3) — but neither can join one. Both take the hive's write connection directly and commit work of their own, which is why a bound transaction makes them decline rather than wait (§4.1).

5.6.1 RSI_DELETE_LAYER #

Removes every entry belonging to one layer and reports the keys that the removal left unreferenced.

The operation is applied to every registered hive, not to one identified from the request, and the per-hive orphan sets are concatenated into a single response array.

For each hive, inside one BEGIN IMMEDIATE transaction, loregd first computes the orphan set and then deletes:

-- GUIDs referenced by the layer being removed
SELECT DISTINCT target_guid FROM main.path_entries
WHERE layer = ? AND target_type = 0
UNION
SELECT DISTINCT target_guid FROM volatile.path_entries
WHERE layer = ? AND target_type = 0

minus the GUIDs referenced by any other layer, gathered the same way with layer != ?. What remains is reachable only through the layer being deleted, and is therefore orphaned by it.

DELETE FROM path_entries       WHERE layer = ?;
DELETE FROM [values]           WHERE layer = ?;
DELETE FROM blanket_tombstones WHERE layer = ?;

Both schemas are covered, and all six deletions run inside the same transaction as the orphan computation, so nothing can be inserted between the two steps. The orphaned GUIDs are evicted from the hive cache (§4.2) and returned to the caller.

The response array is sorted into ascending byte order (§5.2).

A contended write is reported as RSI_TXN_BUSY, the same distinction every other write path draws: the operation declines if any transaction already holds a hive's write connection, and again if a hive's own BEGIN IMMEDIATE finds the database busy. Other failures are RSI_STORAGE_ERROR.

5.6.2 RSI_FLUSH #

Forces the hive's write-ahead log to be checkpointed so that all persistent data is durable on disk:

PRAGMA wal_checkpoint(TRUNCATE)

The request carries a hive name rather than a GUID. It is matched case-insensitively against the registered hives by folded name (§3.4). A name matching none returns RSI_INVALID.

Two conditions return RSI_TXN_BUSY instead of checkpointing:

  • Any transaction is currently bound to the hive. A checkpoint on a connection already held by a transaction would deadlock, so loregd declines immediately rather than waiting (§4.4).
  • The checkpoint itself reports that it could not complete because the database was busy.

The volatile store has no durability and is unaffected: nothing is flushed, and nothing needs to be.

5.7 Status Codes

Peios / Peios technical reference / loregd / Request Handling

loregd returns seven of the RSI status codes:

StatusValueReturned when
RSI_OK0The operation succeeded. Also returned by the idempotent deletions when their target was already absent.
RSI_NOT_FOUND1A named key GUID exists in neither store, or resolves to no registered hive. Also an update matching no row.
RSI_ALREADY_EXISTS2A duplicate key GUID or path entry: the target table's primary key, or the matching check against the other store (§5.3, §5.4). No value operation produces it — every value write is an INSERT OR REPLACE.
RSI_STORAGE_ERROR3A SQLite failure while serving the request. Also an operation whose GUID belongs to a hive other than the one its transaction is bound to, a mutating operation carrying an unknown transaction id, and a failed commit that was not busy.
RSI_TXN_BUSY6BEGIN IMMEDIATE found the database busy, a conditional write could not begin its transaction, or RSI_FLUSH or RSI_DELETE_LAYER found a transaction already bound to a hive's write connection.
RSI_INVALID7An unknown opcode, a request whose payload cannot be decoded, an out-of-range RSI_WRITE_KEY field mask, a mutating operation carrying a read-only transaction id, an RSI_FLUSH naming an unregistered hive, or an RSI_BEGIN_TRANSACTION re-using an active id or naming a mode other than RSI_TXN_READ_WRITE or RSI_TXN_READ_ONLY.
RSI_CAS_FAILED8A conditional RSI_SET_VALUE whose target was absent or whose sequence did not match.

Three codes are defined by the interface and never produced by loregd: RSI_NOT_EMPTY (4), RSI_TOO_LARGE (5), and RSI_TXN_NOT_SUPPORTED (9).

RSI_TXN_NOT_SUPPORTED is never needed because loregd supports both transaction modes (§4.3).

RSI_TOO_LARGE is never returned because an oversized or malformed frame is not answered at all. Framing is validated before an opcode is known, and a frame that fails validation ends the connection instead of producing a response (§2.3).

Appendix A Prior Art

Peios / Peios technical reference / loregd

A.1 SQLite #

loregd's storage engine is SQLite, and it is specified against SQLite rather than against an abstract store. The schema, the concurrency model, and the operational behaviour all name SQLite features directly: WAL mode for concurrent readers alongside a serialised writer, savepoints and transactions for atomicity, PRAGMA wal_checkpoint for durability on demand, and its crash recovery for restart after an unclean shutdown.

Both halves of a hive are SQLite. Persistent data lives in the database file named on the command line; volatile data lives in a second, tmpfs-backed WAL database attached to the same connections (§3.3). Using one engine for both is what makes a transaction spanning persistent and volatile data atomic without any additional machinery.

A different storage engine would have to supply equivalent transactional and concurrency semantics. Nothing in the design forbids that, but nothing accommodates it either.

A.2 The Windows registry hive format #

The Windows registry stores hives as binary REGF files managed directly by the kernel's Configuration Manager. loregd departs from that model completely: storage is a SQLite database managed by an unprivileged userspace daemon, not a kernel-managed binary file, and the kernel holds no storage of its own at all.

What the two share is the data model — keys, values, security descriptors — which Peios inherits through the registry's kernel-side specification rather than from the file format. The on-disk format has no relationship to REGF whatsoever, and no REGF file can be read by loregd or written by it.

A.3 The Registry Source Interface #

loregd implements the RSI, which is specified in PSPK rather than here. That specification defines the operations, the message format, the error model, and the obligations binding on any source; loregd's request handling (chapter 5) is a mapping of those operations onto SQL against the two stores.

Where this manual and the RSI specification disagree about wire behaviour, the specification is correct and this manual has a bug — the RSI is a contract with the kernel, and loregd is one implementation of one side of it.

Peios Learn — documentation for the Peios project.

Built with Trail.