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:
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 reportwal, 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_registryreturns 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.
SIGTERMandSIGINTare 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.
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 :
(
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:
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 #
(
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
);
3.2.2 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)
);
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.
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 #
[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)
);
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 #
(
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.
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.
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=walon 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_KEYstores 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_KEYevicts immediately outside a transaction, or through a commit hook inside one.RSI_DELETE_LAYERevicts 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:
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_VALUESresponse comes from its ownUNION ALL, separate from the value entries it travels beside, and takes the same order they do. - The orphan-GUID array in an
RSI_DELETE_LAYERresponse 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:
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:
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.