# Peios System Protocols Kernel

> The contracts spoken across the kernel boundary — the protocols a Peios kernel subsystem depends on a userspace process to fulfil, and the formats it exchanges with one.

---

# 1.1 Scope

_Peios / Advanced Peios / PSPK / Introduction_

> What PSPK is — the protocols spoken across the kernel boundary between a kernel subsystem and the userspace process serving it — and how trust runs across that line.

This document defines the **Peios System Protocols Kernel (PSPK)**: the
protocols spoken across the kernel boundary, between a kernel subsystem
and a userspace process that serves it.

A contract belongs in this document when both of these hold:

- one party is a kernel subsystem and the other is a userspace process;
  and
- the userspace side is a public, implementable role — a third party can
  write a program that fills it.

The two parties need not be in conversation. A live protocol has a
kernel subsystem and a process exchanging messages; a **format** has one
side producing an artifact that the other consumes, perhaps long
afterwards and on a different machine. Both are specified here, because
both are contracts a third party has to satisfy exactly.

The second condition is what separates a PSPK protocol from a system-call
surface. A system call is an interface the kernel offers to any program
that asks. A PSPK protocol is a contract the kernel depends on some
program to fulfil: the kernel is the party asking, and the userspace
process is authoritative for the answer.

For each protocol, this document covers:

- the channel, and how a userspace party attaches to it and is
  recognised
- message or artifact framing, encoding, and the rules under which the
  format may be extended
- the requests the kernel issues, the responses it expects, and their
  ordering
- what the kernel-side party validates for itself rather than believing
  from a response
- behaviour on failure, refusal, and disconnection
- the conformance requirements for the userspace role

This document does not cover:

- The behaviour and data model of the kernel subsystem itself — defined
  in that subsystem's specification
- System-call and ioctl surfaces — defined in that subsystem's
  specification
- The binary structures these protocols carry — defined in PCDS
- Standards a system MUST implement to be Peios — defined in PGSS
- Protocols between userspace components — defined in PSPU
- How a userspace implementation stores its data or computes its
  answers — its own design

## 1.1.1 Trust across the boundary

A PSPK protocol crosses a trust boundary in the direction that matters
most: a kernel subsystem is asking a lower-privileged process for
something it will then act on. Every specification in this document
therefore states explicitly which parts of a response the kernel
establishes for itself and which it takes on the userspace party's word.

## 1.1.2 Relationship to PGSS

A protocol in this document is not a conformance requirement in the sense
PGSS defines. It is the interface a particular Peios kernel subsystem
uses to reach the processes that serve it; a system built from different
kernel subsystems is still Peios. These protocols are specified because
they are public even so — a third party writing an implementation of the
userspace role needs the contract written down.

---

# 1.2 Conventions

_Peios / Advanced Peios / PSPK / Introduction_

> The normative keywords PSPK uses, and the shared conventions it inherits from the PCSA conventions book.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this
document are to be interpreted as described in RFC 2119. Text set off as
a note is informative, not normative.

Everything else — roles, byte order, sizes, layout tables, notation,
strings, timestamps, citation, and the external standards this anthology
depends on — is defined in the Conventions book and is not restated
here. PSPK departs from none of it.

Where a chapter needs a convention of its own, that chapter states it.

---

# 2.1 Scope and Roles

_Peios / Advanced Peios / PSPK / KMES Event Stream_

> The KMES contract and its two roles — the kernel as sole producer, userspace as consumer — and the privilege that gates attachment.

This chapter specifies the contract between the Kernel Mediated Event
Subsystem and the userspace processes that consume the events it
produces.

Two roles participate.

The **producer** is KMES, a subsystem of the Peios kernel. It
constructs events, stamps them with metadata that a consumer cannot
forge, places them in per-CPU shared memory ring buffers, and notifies
sleeping consumers. There is one producer.

The **consumer** is a userspace process that maps one or more ring
buffers and reads events from them. The consumer role is publicly
implementable: any process holding the required privilege MAY attach,
and more than one consumer MAY attach to the same buffer at the same
time. A conforming consumer is the subject of the requirements in this
chapter.

This chapter covers:

- the binary layout of an event, which a consumer MUST parse
- the layout of a mapped ring buffer and the meaning of each metadata
  field
- how a consumer attaches, maps, and discovers the buffer set
- the protocol a consumer follows to drain events, to detect and
  account for loss, and to sleep and be woken
- the protocol a consumer follows when the producer replaces a buffer
- the memory ordering both roles rely on

This chapter does not cover:

- how KMES constructs, stamps, buffers, or overwrites events — the
  producer's internals are described in the Peios Kernel TRM
- the emission interfaces, by which a process or kernel subsystem
  produces an event rather than consuming one
- event type vocabulary, payload schemas, persistence, indexing, or
  querying — these are the concern of the event storage service
- the encoding of payload bytes beyond their being a single MessagePack
  value

## 2.1.1 Producing versus consuming

Emission is not part of this contract. A process emits events by
calling the KMES emission system calls, which are an ordinary kernel
interface offered to any caller that holds the privilege — the kernel
computes the result and the caller reads it. Consumption is different:
the kernel deposits bytes in shared memory and depends on an
independently written program to interpret them correctly, to
sequence its reads against concurrent writes, and to notice when it
has fallen behind. That program's obligations have to be written down,
which is why they are here.

## 2.1.2 What the kernel establishes for itself

A consumer maps one page that it can write: the consumer metadata
page. Everything the kernel reads from that page is advisory.

KMES reads exactly one field from consumer-writable memory, the
`need_wake` flag, and treats any nonzero value as set. The flag can
only cause KMES to perform a wake that was not needed or to skip one
that was; it cannot affect the contents of the data region, the
producer metadata, the sequence numbering, or another consumer's view
of any of these. A consumer that corrupts the page — deliberately or
otherwise — degrades notification for consumers sharing that buffer
and nothing else.

Consumers MUST NOT rely on KMES validating anything else they write,
because KMES reads nothing else.

The reverse direction is stronger. The producer metadata page and the
data region are mapped read-only, and no privilege, capability, or
token grants a consumer write access to them. Every identity stamp in
an event header is captured by the kernel from kernel state at the
moment of the write; an emitting process cannot set, influence, or
suppress it. A consumer MAY therefore treat the identity fields of a
delivered event as authoritative.

## 2.1.3 Privilege and the trust model

Attaching requires SeSecurityPrivilege, which is a very high-trust
privilege. Direct ring buffer access is not the ordinary way to
consume events: it grants an unfiltered view of every event on the
system, with no per-event access control. Ordinary consumers obtain
events from the event storage service, which enforces per-event
access control on top of this interface.

Because the consumer metadata page is shared by every consumer
attached to a buffer, a consumer holding SeSecurityPrivilege can
suppress notification for the others attached to that buffer. This is
accepted rather than defended against: the privilege required to
attach at all is higher than the privilege this would subvert.

---

# 2.2 Event Format

_Peios / Advanced Peios / PSPK / KMES Event Stream_

> An event is one contiguous record — the packed header layout, the payload, event types, origin class, identity fields and ordering.

An event is an indivisible record: a packed binary header followed
immediately by a payload. Header and payload are always stored,
delivered, and consumed as one contiguous byte sequence, and neither
is meaningful alone.

## 2.2.1 Header layout

The header fields are laid out sequentially with no padding and no
alignment gaps. All multi-byte integers are little-endian.

| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 4 | `u32` | `event_size` | Total size of the event, header plus payload, in bytes. |
| 4 | 4 | `u32` | `header_size` | Size of the header in bytes. |
| 8 | 8 | `u64` | `timestamp` | Wall clock time at emission, in nanoseconds since the Unix epoch. |
| 16 | 8 | `u64` | `sequence` | Per-CPU, per-boot monotonic sequence number. |
| 24 | 2 | `u16` | `cpu_id` | The CPU on which the event was emitted, identifying the ring buffer that carries it. |
| 26 | 1 | `u8` | `origin_class` | The emission path that produced the event. |
| 27 | 16 | `GUID` | `effective_token_guid` | GUID of the effective token of the emitting thread. Null GUID if unavailable. |
| 43 | 16 | `GUID` | `true_token_guid` | GUID of the emitting process's primary token. Null GUID if unavailable. |
| 59 | 16 | `GUID` | `process_guid` | GUID of the emitting process. Null GUID if unavailable. |
| 75 | 2 | `u16` | `type_len` | Length of the event type string in bytes. |
| 77 | `type_len` | `[u8]` | `type` | Event type string, UTF-8, not null-terminated. |

GUIDs use the binary format defined in PCDS and are opaque 16 bytes to
this contract. The **null GUID** is sixteen zero bytes and means the
field is not applicable or was not available.

`header_size` is `77 + type_len` in this version of the format. A
consumer MUST use `header_size` to locate the payload and MUST NOT
compute the payload offset from `77 + type_len` or from any other
constant, so that a future header extension does not break it. All
fields before `type` are at fixed offsets and will remain so.

The payload occupies the bytes from `header_size` to `event_size`. The
next event in a ring buffer begins `event_size` bytes after the start
of the current one, with no alignment padding between events.

## 2.2.2 Payload

The payload is exactly one MessagePack value, and its structure is
defined by the emitter. KMES does not interpret it.

A consumer MUST NOT assume a payload is present: an event emitted by a
kernel subsystem MAY have `event_size == header_size`, meaning a
header and no payload at all. Events emitted through the system calls
always carry a payload, because an empty byte sequence is not a valid
MessagePack value and is rejected at the syscall boundary.

Note that MessagePack encodes its own length prefixes big-endian,
whereas every integer in the event header is little-endian. Both
appear in one event.

## 2.2.3 Event types

The event type is an arbitrary UTF-8 string. KMES imposes no
structure, namespace, or naming convention on it, and applies no case
folding or normalisation. Consumers MUST compare event types as raw
byte sequences.

## 2.2.4 Origin class

| Value | Origin |
|---|---|
| 0 | Userspace, via system call |
| 1 | KMES |
| 2 | KACS |
| 3 | LCS |

Values 4–255 are unassigned. A consumer MUST tolerate an unrecognised
origin class rather than rejecting the event, so that a kernel
subsystem added later does not break it.

Events with origin class 0 were emitted through the system call
interface, and their origin class is set by the kernel, not by the
caller. A userspace emitter cannot claim to be a kernel subsystem.

## 2.2.5 Identity fields

The three identity GUIDs are captured by the kernel at the moment the
event is written to the ring buffer, not when the emitting call began.

- `effective_token_guid` is the token governing the emitting thread's
  access rights. If the thread was impersonating, this is the
  impersonation token; otherwise it equals `true_token_guid`.
- `true_token_guid` is the emitting process's primary token,
  regardless of impersonation.
- `process_guid` identifies the emitting process. It is assigned when
  the process is created and does not change across `exec`.

Any of the three MAY be the null GUID, meaning the kernel had no
identity to record — emission before the access control subsystem
initialised, or from a context with no associated process such as a
kernel worker thread. A consumer MUST treat a null identity as
"unattributed" and MUST NOT treat it as a valid GUID value that could
match a real token or process.

## 2.2.6 Ordering

Events from different CPUs are ordered by `timestamp`. Events with
identical timestamps from different CPUs were genuinely concurrent and
have no defined relative order.

Within a single CPU, `sequence` is the ordering primitive. It is
monotonic across wall clock discontinuities, which `timestamp` is not:
a clock adjustment can move timestamps backwards, and a consumer that
requires monotonic ordering within a CPU MUST use `sequence`. Events
with identical timestamps on the same CPU are ordered by `sequence`.

Each CPU numbers independently and there is no global sequence. The
counter starts at zero when the kernel module loads and is incremented
before a value is taken, so the first event on a CPU carries sequence
number 1 and **sequence 0 is never assigned**. The pair
(`cpu_id`, `sequence`) uniquely identifies an event within one boot.

A gap in the sequence for a given CPU means events were lost — either
overwritten before the consumer read them, or dropped by the kernel
before they reached the buffer. Sequence numbers are continuous across
a buffer replacement, so a generation change does not itself produce a
gap.

---

# 2.3 Attaching and Mapping

_Peios / Advanced Peios / PSPK / KMES Event Stream_

> How a consumer attaches to a per-CPU ring, the double mapping that makes wrapping invisible, and the producer metadata page.

## 2.3.1 Attaching

A consumer attaches to one per-CPU ring buffer at a time by calling
`kmes_attach` with a logical CPU index and a pointer to a `u64` that
receives the buffer's capacity. The call returns a file descriptor.

The caller MUST hold SeSecurityPrivilege, enabled; the call fails with
`EPERM` otherwise.

The CPU index uses the same numbering as the `cpu_id` field in the
ring buffer metadata and in event headers. Indexes run from 0 to one
below the *slot count*, and the set of buffers is fixed when KMES
initialises and does not change while the system runs.

The slot count is not the number of buffers. Slots are indexed by
logical CPU id, so a slot within the range holds no buffer when that
CPU is not possible, and the two quantities differ on any system whose
possible-CPU mask is sparse. An index at or beyond the slot count fails
with `EINVAL`, and so does an index inside it whose slot holds no
buffer; the two are not distinguishable from the return value.

A consumer discovers the slot count by calling `kmes_attach` with the
CPU index `KMES_ATTACH_QUERY_SLOTS`. The call writes the slot count
through the capacity pointer, returns 0, and opens no descriptor. It is
gated on SeSecurityPrivilege exactly as an attach is.

A consumer MUST enumerate by walking every index from 0 to one below
the slot count, and MUST treat `EINVAL` as "this slot holds no buffer"
and continue. A consumer MUST NOT treat the first `EINVAL` as the end
of the set: doing so silently abandons every buffer above the first
hole, whose events then accumulate and are overwritten with no consumer
able to reach them.

A consumer SHOULD attach to every buffer in the set: a buffer with no
consumer still receives events, and those events are lost when it
wraps.

A consumer MAY call `kmes_attach` more than once for the same CPU and
receives a distinct file descriptor each time. All descriptors for one
CPU refer to the same buffer, and therefore to the same producer
metadata, consumer metadata, and data region. Multiple consumers MAY
attach to one buffer concurrently. Each maintains its own read
position, in its own memory; the kernel does not track consumer read
positions, does not know how many consumers exist, and does not know
how far behind any of them is. The consumer metadata page is shared
per buffer and is not a per-consumer read-position store.

The descriptor supports exactly two operations: `mmap()` and
`close()`. Closing it invalidates the mapping.

## 2.3.2 Mapping

The consumer maps the whole region in a single call:

```
mmap(NULL, 8192 + 2 * capacity, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
```

The mapping request MUST use `MAP_SHARED`, MUST pass an offset of
zero, and MUST pass a length of exactly `8192 + 2 * capacity`, where
`capacity` is the value `kmes_attach` returned. Any other combination
fails with `EINVAL`. The consumer does not map the regions separately.

The mapped region has three parts:

| Offset | Size | Region | Consumer access |
|---|---|---|---|
| 0 | 4096 | Producer metadata page | Read-only |
| 4096 | 4096 | Consumer metadata page | Read-write |
| 8192 | 2 × capacity | Data region | Read-only |

Per-page permissions are enforced by the kernel regardless of the
`PROT` flags requested: the producer metadata page and the data region
are mapped read-only whatever the consumer asks for, and no privilege
raises that. The consumer metadata page is writable by ordinary
stores.

`capacity` is always a power of two, so a position's offset within the
data region is `position & (capacity - 1)`.

## 2.3.3 The double mapping

The data region occupies `2 × capacity` bytes of address space backed
by `capacity` bytes of memory: the same pages appear twice,
consecutively. An event that crosses the end of the buffer is
therefore readable as one contiguous byte sequence starting at its
wrapped offset, and a consumer MUST NOT implement wrap handling of its
own. Reading an event at offset `position & (capacity - 1)` is always
correct, even when the event extends past `capacity`.

A consumer MUST NOT read past `event_size` bytes from the start of an
event. The data region is not scrubbed when events are overwritten, so
the bytes beyond an event are unrelated remnants of older events.

## 2.3.4 Producer metadata page

The producer metadata page is written by KMES and read by consumers.
Fields are separated onto 64-byte cache lines by update frequency, so
that the position fields KMES writes on every event do not invalidate
the line holding the fields it never writes.

### 2.3.4.1 Bytes 0–63: identification

| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 8 | `[u8; 8]` | `magic` | `4B 4D 45 53 52 49 4E 47`, `KMESRING` in ASCII. Compared byte by byte, not as an integer. |
| 8 | 4 | `u32` | `version` | Ring buffer format version. This version is 1. |
| 12 | 2 | `u16` | `cpu_id` | The CPU this buffer belongs to. |
| 14 | 2 | `u16` | `reserved0` | Reserved, zero. |
| 16 | 8 | `u64` | `capacity` | Data region capacity in bytes. A power of two. |
| 24 | 8 | `u64` | `data_offset` | Offset from the start of the mapping to the data region. 8192. |
| 32 | 8 | `u64` | `generation` | Buffer generation. Starts at 1 for the first buffer on each CPU and increases by one each time the buffer is replaced. |
| 40 | 24 | -- | `reserved1` | Reserved, zero. |

A consumer MUST verify `magic` and `version` before trusting any other
field in the mapping.

A consumer MAY cache `magic`, `version`, `cpu_id`, `capacity`, and
`data_offset` for the lifetime of the mapping; these do not change
once the buffer exists. A consumer MUST NOT cache `generation`: it
shares this cache line but is written when the buffer is superseded,
and re-reading it is how a consumer learns that it must re-attach.

### 2.3.4.2 Bytes 64–127: positions

| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 64 | 8 | `u64` | `write_pos` | Monotonically increasing byte offset at which the next event will be written. Never wraps. |
| 72 | 8 | `u64` | `tail_pos` | Byte offset of the oldest surviving event. Advanced by KMES as events are overwritten. |
| 80 | 48 | -- | `reserved2` | Reserved, zero. |

Both are absolute byte offsets that increase without bound; the
corresponding data region offset is the value masked with
`capacity - 1`. A `u64` byte offset does not overflow in any practical
deployment — at a sustained gigabyte per second it would take over
five hundred years — and consumers MUST NOT implement wrap handling
for these counters.

### 2.3.4.3 Bytes 128–191: notification

| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 128 | 4 | `u32` | `futex_counter` | Incremented by KMES when it wakes sleeping consumers. |
| 132 | 60 | -- | `reserved3` | Reserved, zero. |

The counter is 32-bit because the Linux futex operates on 32-bit
integers, and it is incremented only when `need_wake` is set.

## 2.3.5 Consumer metadata page

| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 4096 | 1 | `u8` | `need_wake` | Set by a consumer that is about to sleep. Read by KMES after writing an event; any nonzero value counts as set. |
| 4097 | 4095 | -- | `reserved4` | Reserved. |

This page is shared by every consumer attached to the buffer. A
consumer MUST NOT store per-consumer state on it, and in particular
MUST NOT store its read position there.

Consumers MUST NOT write to any offset in this page other than
`need_wake`. Reserved bytes are reserved for future extension of this
contract.

---

# 2.4 Consumer Protocol

_Peios / Advanced Peios / PSPK / KMES Event Stream_

> The consumer protocol — draining a ring, detecting loss, waiting for notification, handling buffer replacement, and the memory ordering it all rests on.

A consumer typically dedicates one thread to each buffer. Each thread
independently drains its buffer, sleeps when the buffer is empty, and
re-attaches when the buffer is replaced. The protocol uses no locks
and, while events are available, no system calls.

Each consumer keeps its own `read_pos` in its own memory. On first
attaching to a buffer, a consumer SHOULD set `read_pos` to the
buffer's current `tail_pos`, which starts it at the oldest surviving
event.

## 2.4.1 Draining

1. Load `write_pos` with acquire ordering. If `write_pos == read_pos`,
   no events are available: go to the notification wait.
2. Load `tail_pos` with acquire ordering. If `read_pos < tail_pos`,
   the events at the read position have been overwritten and the
   consumer has been lapped: set `read_pos = tail_pos`. The skipped
   span is lost, and will show up as a sequence gap.
3. Save the current `tail_pos` as `saved_tail`.
4. Read the event at data region offset `read_pos & (capacity - 1)`.
5. Re-read `tail_pos`. If it has advanced past `saved_tail` and
   `read_pos < tail_pos`, the event was overwritten while it was being
   read. The bytes just read MUST be discarded: go to step 2.
6. Check that `event_size > 0` and `event_size >= header_size`. If
   either fails, the bytes are not a valid event; the consumer SHOULD
   set `read_pos = tail_pos` and go to step 2. A consumer MUST perform
   this check: an `event_size` of zero would otherwise make the drain
   loop spin forever.
7. Process the event. Advance `read_pos` by the event's `event_size`.
   Go to step 1.

A consumer MUST NOT read beyond an event's `event_size` boundary.

Steps 3 and 5 are what make a lock-free read safe against a producer
that is overwriting the region being read. A consumer that omits the
re-read can process a torn event assembled from two different events'
bytes.

## 2.4.2 Detecting loss

A consumer SHOULD track the last sequence number it processed for each
CPU. A gap means events were lost, whether because they were
overwritten before being read or because the kernel dropped them
before they reached the buffer. The size of the gap is the number of
events lost.

Loss is a normal condition under load, not an error: the buffer
preserves recent events at the cost of old ones. A consumer SHOULD
report loss rather than treating it as fatal.

## 2.4.3 Notification wait

When a buffer is empty:

1. Store 1 to `need_wake` with release ordering.
2. Re-load `write_pos` with acquire ordering. If events arrived
   between the drain loop finding the buffer empty and this store,
   clear `need_wake` to 0 and return to the drain loop. This re-check
   is REQUIRED: without it, an event written in that window would find
   `need_wake` still clear, and the consumer would sleep with events
   waiting.
3. Read `futex_counter`.
4. Optionally spin, re-checking `write_pos`. If events arrive during
   the spin, clear `need_wake` to 0 and return to the drain loop. The
   spin duration is the consumer's choice, and a consumer MAY omit
   this step entirely.
5. Call `futex_wait(&futex_counter, last_seen_value)`, where
   `last_seen_value` is the value read in step 3. The kernel puts the
   thread to sleep only if `futex_counter` still holds that value, so
   a wake that arrived in the meantime is not missed.
6. On waking, clear `need_wake` to 0 and return to the drain loop.

The futex address is the `futex_counter` field in the mapped producer
metadata page. This is a **shared** futex, keyed by the page's backing
inode, and a consumer MUST wait on it as such: a wait issued with
`FUTEX_PRIVATE_FLAG` will never be woken.

Clearing `need_wake` to 0 in steps 2, 4, and 6 MAY be a relaxed store.
If KMES reads a stale set value after the consumer has cleared it, it
performs a wake on a thread that is already awake, which is harmless.

Under sustained load a consumer never reaches the notification wait:
it stays in the drain loop, `need_wake` stays 0, and the producer's
notification cost is a single byte read per event.

## 2.4.4 Buffer replacement

KMES replaces every buffer when its configured capacity changes.
Replacement preserves as many surviving events as the new capacity
allows and keeps sequence numbering continuous, but it invalidates
positions: the events are re-compacted from position 0 in the new
buffer, so the consumer's `read_pos` means nothing there.

After each drain cycle — the buffer emptied, or a batch limit reached
— a consumer SHOULD read `generation`. If it differs from the value
last seen:

1. Record the sequence number of the last event successfully processed
   from this buffer.
2. Finish draining the old buffer up to its `write_pos`, which is now
   frozen: KMES has stopped writing to it. A consumer MUST complete
   this drain before switching, or it loses every event written
   between its read position and the switchover.
3. Call `kmes_attach` for the same CPU to obtain a descriptor for the
   replacement buffer, and map it.
4. Read the new buffer's `capacity`, `write_pos`, and `tail_pos`.
5. Scan the new buffer for the first event whose sequence number is
   greater than the recorded one, and set `read_pos` to that event's
   position. A consumer MUST locate its position by sequence number
   and MUST NOT carry `read_pos` across.
6. Close the old descriptor and unmap the old buffer.
7. Resume draining from the new buffer.

The old buffer's pages remain valid for as long as any consumer keeps
them mapped, so a consumer is never racing to finish before the memory
disappears.

If the new capacity is large enough to hold everything that survived
in the old buffer, no events are lost across the replacement. If it is
smaller, the oldest surviving events are discarded until the remainder
fits — the same overwrite semantics applied against the smaller
capacity — so loss is bounded to the oldest part of the buffer and
appears as a sequence gap.

A consumer sleeping on the old buffer is woken when the replacement
happens, provided its `need_wake` was set, so it observes the
generation change rather than sleeping indefinitely on a buffer that
will never receive another event.

## 2.4.5 Memory ordering

| Operation | Ordering | Purpose |
|---|---|---|
| Producer stores `tail_pos` | release | The advanced tail is visible before the data that replaces the events it skipped past. |
| Producer stores `write_pos` | release | Complete event data is visible before the position that makes it reachable. |
| Producer stores `futex_counter` | release | A consumer waking from the futex observes all prior writes. |
| Consumer stores `need_wake = 1` | release | The producer observes the flag before the consumer waits. |
| Consumer stores `need_wake = 0` | relaxed | A stale read causes only a spurious wake. |
| Consumer loads `write_pos` in the drain loop | acquire | Pairs with the producer's release. |
| Consumer loads `write_pos` after setting `need_wake` | acquire | Closes the window between finding the buffer empty and announcing the sleep. |
| Consumer loads `tail_pos` | acquire | Pairs with the producer's release. |

For a given buffer there are exactly two kinds of party: one producer,
which is the kernel on the owning CPU, and any number of consumers.
There is no multi-producer contention to account for.

On x86-64 the producer's release stores compile to plain stores,
because the architecture does not reorder stores with other stores.
Consumers MUST NOT rely on that: the ordering above is required for
correctness on weaker architectures, and a consumer written without it
is incorrect on those machines whether or not it is observed to fail
on x86-64.

---

# 3.1 Scope and Roles

_Peios / Advanced Peios / PSPK / Binary Signing and PIP_

> How a binary is signed so the Peios kernel will accept it, who the signer and verifier are, and why signing is a contract rather than an implementation detail.

This chapter specifies how a binary is signed so that the Peios kernel
will accept it, and what the trust level a signature confers means.

Two roles participate.

The **signer** is a userspace program holding a private key. It
computes a content hash over a file, signs it, and attaches the
signature to that file. The signer role is publicly implementable: a
third party building software for Peios, or an organisation operating
its own trust tier, MUST be able to produce an acceptable signature
from this chapter alone.

The **verifier** is the Peios kernel. It carries public keys, checks
signatures at execution and at library load, and derives a process's
Process Integrity Protection identity from whichever key verified. The
verifier role is not publicly implementable and is not specified here;
this chapter constrains it only where the signer needs guarantees
about what it will do.

This chapter covers:

- the signature blob's encoding
- where a signature is stored, and the order in which storage
  locations are consulted
- exactly which bytes are covered by the signature
- the signature algorithm, its parameters, and the absence of domain
  separation
- how a key is selected during verification, and how a trust tier
  follows from it
- the meaning of a PIP identity, and what a signer is asserting by
  requesting one
- the guarantees a signer may rely on, and the ones it may not

This chapter does not cover:

- how the kernel parses, hashes, or verifies — described in the Peios
  Kernel TRM
- how PIP is enforced between processes or against objects — likewise
  the TRM's concern
- key generation, custody, rotation, or distribution policy
- the process mitigations that compose with PIP, which are set by a
  process launcher and are unrelated to signing

## 3.1.1 Why signing is a contract and not an implementation detail

A binary's trust level is not something a process can request. There is
no runtime interface that confers PIP, no inherited grant from a
parent, and no flag at process creation. The **only** input is the
signature on the file being executed, and the only authority is the
key that verifies it.

That makes the signature the entire boundary. A signer that produces
a byte-for-byte correct blob over the correct bytes obtains a trust
tier; one that gets any of it wrong obtains none, silently, because an
unverifiable binary executes with no protection rather than failing to
execute. There is no error to observe and no diagnostic to read.

A specification is therefore the only thing standing between a signer
and a silent, total loss of the property it was trying to obtain.

## 3.1.2 What the kernel establishes for itself

The verifier trusts nothing in the artifact except the signature's
arithmetic.

The trust tier is **not** carried in the signature, the file, or any
metadata a signer controls. It is a property of the key that verified,
looked up in a table compiled into the kernel image. A signer cannot
encode, request, or influence the tier it receives; presenting a
signature made with a key the kernel does not carry is
indistinguishable from presenting no signature at all.

The signature covers the file's content, and the verifier re-derives
the content hash itself over a stable size snapshot rather than
trusting any length or digest recorded in the artifact.

A verified file is pinned against in-place modification for as long as
its inode lives, so a signer MUST NOT assume it can update signed
content in place. Replacement by a new inode is the only supported
update path.

## 3.1.3 Relationship to PGSS

Binary signing is not a conformance requirement in the sense PGSS
defines. A system that verifies no signatures, or verifies them
against different keys, is still Peios; PIP is additive protection
rather than a property every Peios system exhibits.

It is specified here because the contract is public even so. A third
party that wants its software to run at a trust tier — or that wants
to operate a tier of its own — needs the format written down exactly,
and needs to know which of the verifier's behaviours it may depend on.

---

# 3.2 Signature Format

_Peios / Advanced Peios / PSPK / Binary Signing and PIP_

> The fixed 3310-byte signature blob — where it is stored, the lookup order, what is covered, and how a key is selected and trusted.

## 3.2.1 The blob

A signature is a fixed 3310-byte blob, identical wherever it is
stored:

| Offset | Size | Field |
|---:|---:|---|
| 0 | 1 | Version. MUST be `0x01`. |
| 1 | 3309 | Raw ML-DSA-65 signature. |

Total 3310 bytes exactly. There is no length field, no algorithm
identifier, no key identifier, no timestamp and no padding. A blob of
any other length MUST be rejected, and so MUST a version byte other
than `0x01`.

Signers MUST NOT emit any other version. A verifier encountering one
MUST treat the file as unsigned rather than attempting a fallback
interpretation.

## 3.2.2 Storage

Two locations are defined. A verifier MUST consult them in this order.

### 3.2.2.1 ELF section

An ELF binary SHOULD carry its signature in a section named exactly
`.peios.sig`. The name comparison covers all eleven bytes including
the terminating NUL, so a longer name having `.peios.sig` as a prefix
MUST NOT match.

The section's type MUST be `SHT_PROGBITS` and its size MUST be exactly
3310. The range `[sh_offset, sh_offset + sh_size)` MUST lie entirely
within the file.

The containing file MUST be `ELFCLASS64`, MUST be `ELFDATA2LSB`, and
MUST carry `EV_CURRENT` in `e_ident[EI_VERSION]`. `e_shentsize` MUST
equal 64. `e_shstrndx` MUST NOT be `SHN_UNDEF` and MUST be less than
`e_shnum`. The section header table and the section-name string table
MUST both lie entirely within the file.

No alignment or flag requirement applies. `sh_addralign`, `sh_flags`,
`sh_addr`, `sh_link` and `sh_info` are not inspected, and the
section's index and position are unconstrained. Where several sections
share the name, the one at the lowest index is used.

A 32-bit or big-endian ELF cannot carry a signature at all: it fails
the structural requirements above, and a verifier MUST NOT fall back
to the extended attribute for it.

### 3.2.2.2 Extended attribute

Any file MAY carry its signature in the extended attribute
`security.peios.sig`. The value MUST be exactly 3310 bytes; any other
size MUST be treated as unsigned.

This is the only location available to non-ELF files, and it is
available to ELF files that carry no `.peios.sig` section header.

### 3.2.2.3 Lookup order and commitment

A verifier MUST determine the storage location as follows.

1. Read the first four bytes. A file shorter than four bytes, or whose
   first four bytes are not `\x7fELF`, is not ELF: go to step 3.
2. Parse the ELF structures and scan for a section named
   `.peios.sig`. **Once such a section header is found, the ELF path
   is committed**: the extended attribute MUST NOT be consulted,
   whatever happens next. A wrong type, a wrong size, an out-of-range
   offset, a read failure, a bad version byte or a failed verification
   all yield "unsigned". A structural failure encountered while
   parsing MUST commit the path in the same way. The single exception
   is `e_shnum == 0`, which does not commit.
3. Read `security.peios.sig`. If present and exactly 3310 bytes, use
   it.
4. Otherwise the file is unsigned.

The commitment rule is a security requirement rather than an
optimisation. Without it, an attacker able to write a malformed ELF
section could force fallback to whichever location they more easily
controlled.

Where both locations are populated, the ELF section wins. A signer
SHOULD NOT populate both.

## 3.2.3 What is signed

The message is a 32-byte SHA-256 content hash. Which bytes it covers
depends on where the signature is stored, and a signer MUST use the
form matching its chosen location.

**ELF section source.** The hash covers the file with the section's
*contents* replaced by zeros:

```
SHA-256( file[0 .. sh_offset)
       || 0x00 × sh_size
       || file[sh_offset + sh_size .. file_size) )
```

Only the section **contents** are zeroed. The `Elf64_Shdr` entry
describing the section is hashed verbatim, as are the ELF header, the
program headers, the section-name string table and every other byte of
the file. The section header metadata is therefore integrity-protected
along with everything else, which is what stops an attacker relocating
or resizing the signature section without invalidating the signature.

This has a direct consequence for how a signer MUST work. The complete
file layout — including `sh_offset`, `sh_size`, `sh_name` and the
position of the section header table — MUST be final before the hash
is computed. The practical sequence is to reserve a 3310-byte
`.peios.sig` section filled with zeros, finalise the layout, hash the
file as it then stands, sign the hash, and write the blob into the
reserved bytes without touching anything else. Because the reserved
region is already zero, the file as hashed and the file as shipped
differ only in those 3310 bytes.

**Extended attribute source.** The hash covers the entire file with no
exclusions:

```
SHA-256( file[0 .. file_size) )
```

This applies to ELF files reaching the attribute path as well as to
non-ELF ones. The ELF-zeroed form is used **only** when the ELF
section is the signature source.

In both cases `file_size` is a snapshot taken at the start of
verification. A verifier MUST re-check the size before returning and
MUST discard the result if it changed.

## 3.2.4 Algorithm

The signature is **ML-DSA-65** as specified in FIPS 204. The public
key is 1952 bytes and the signature is 3309 bytes.

Signing is:

```
ML-DSA.Sign(private_key, content_hash, ctx = "")
```

and verification is the corresponding `ML-DSA.Verify`.

This is **pure** ML-DSA — FIPS 204 Algorithm 2 and Algorithm 3 — and
not HashML-DSA, the pre-hashing variant. The message happens to be a
32-byte SHA-256 hash, which pure ML-DSA signs directly. A signer MUST
NOT use the pre-hashing variant; a signature produced that way will
not verify.

The context string MUST be empty. Signers MUST NOT set a context.
Verifiers cannot express one, so a signature produced under a
non-empty context simply fails to verify rather than being detected
and reported.

It follows that ML-DSA's context field is **not** available for domain
separation between binary signatures and any other Peios signature
system. Separation MUST come from using distinct keys.

A signer MUST supply the public key as the **raw** 1952-byte key, not
an SPKI-wrapped DER encoding. From OpenSSL 3.5 or later, the raw key
is the trailing 1952 bytes of the DER public key.

## 3.2.5 Key selection and trust tiers

The blob carries no key identifier, so a verifier selects a key by
exhaustive trial: it tries each key in its table in order and takes
the first that verifies.

The trust tier is a property of **which key verified** — a `pip_type`
and a `pip_trust`, both unsigned integers — and never of anything the
signer encoded. A signer cannot express intent about the tier it
wants. It obtains whichever tier the verifier associates with the key
it used, and if the verifier carries no matching key the file is
simply unsigned.

Consequently:

- A signer that wants software to run at a tier MUST have its public
  key present in the verifier's table. Getting a key into that table
  is a deployment question, not a format question.
- A signer MUST NOT assume its signature is portable across systems
  carrying different key tables. The same file may be trusted on one
  and untrusted on another, with no observable difference in the file.
- Verification cost is linear in the number of keys, so a verifier MAY
  reasonably carry few.

---

# 3.3 The PIP Contract

_Peios / Advanced Peios / PSPK / Binary Signing and PIP_

> What a verified signature confers — a PIP type and trust level — how dominance is decided, and what may and may not be relied on.

A verified signature confers a **PIP identity**: a `pip_type` and a
`pip_trust`, both 32-bit unsigned integers, taken from the key that
verified. An unsigned, unverifiable or unrecognised binary confers
type 0 and trust 0, which means no protection.

This chapter states what that identity means to a party outside the
kernel — what a signer is asserting by obtaining one, what an object
owner is asserting by labelling an object, and what may and may not be
relied upon.

## 3.3.1 Dominance

All PIP enforcement reduces to one comparison:

```
dominates(caller, target):
    if target.pip_type == 0:
        return true
    return caller.pip_type  >= target.pip_type
       and caller.pip_trust >= target.pip_trust
```

Both axes are compared numerically. Neither is a closed enumeration:
a value carries no meaning beyond its ordering.

Three type values are conventional — 0 for None, 512 for Protected,
1024 for Isolated — but a specification MUST NOT assume they are the
only ones, and a party evaluating dominance MUST compare numerically
rather than switching on known values.

An unprotected target is dominated by everyone. This is what keeps
ordinary processes universally accessible whatever trust values a
caller carries, and it means PIP restricts access **to** protected
things rather than granting access to trusted callers.

Dominance is binary. A caller either dominates or does not; there is
no partial ordering and no per-operation granularity in PIP itself.

## 3.3.2 What a signer asserts

Obtaining a tier is an assertion about the **binary**, not about what
it will do. Specifically, a signer with a key at some tier asserts
that the signed file is fit to run at that tier, that its contents are
what the signer intended, and that the signer accepts the file being
treated as trusted by every dominance comparison on every system
carrying that key.

A signer MUST NOT treat a tier as a capability grant. A high tier
confers no privilege, no access right and no identity. It protects the
process from lower-tier processes and permits it to reach
higher-protected objects; it grants nothing on its own.

A signer SHOULD understand that a tier is inherited by children at
fork and re-derived at their exec. A protected process that execs an
unsigned binary loses protection entirely — protection follows the
binary, not the lineage.

## 3.3.3 What an object owner asserts

An object opts into PIP protection by carrying a process trust label
in its SACL, whose SID has the form `S-1-19-{type}-{trust}` — the
Process Trust authority with exactly two sub-authorities. A SID of any
other shape makes the descriptor malformed, and an evaluator MUST
reject it rather than guess.

The label's access mask names exactly the rights a **non-dominant**
caller may still receive. A dominant caller is unrestricted by the
label.

Two properties matter to anyone authoring one.

**There is no default.** An object with no trust label is unrestricted
by PIP, reachable by any process whatever its identity. Protection is
opt-in per object.

**Privileges do not compensate.** PIP revokes rights that privileges
granted, including `ACCESS_SYSTEM_SECURITY`. There is no relabel
equivalent, no administrative override, and no privilege that
substitutes for insufficient trust. An object owner labelling an
object may rely on this: a non-dominant caller cannot reach the
object's SACL to remove the label, which is what makes the protection
self-sustaining rather than trivially removable.

## 3.3.4 What may be relied upon

A party may rely on the following.

A tier is derived from the signature alone. No parent process, no
privilege, no runtime interface and no environment can confer,
elevate, or forge one — a compromised process running as SYSTEM cannot
grant PIP to an unsigned binary.

Impersonation does not alter it. PIP is read from per-process state
rather than from a token, so a service impersonating a client still
evaluates its own tier, and a process impersonating a token created
for a protected process gains nothing.

A verified file is pinned against in-place modification for as long as
its inode remains live. Ordinary, positioned and append writes,
truncation by descriptor or pathname, every `fallocate` mode, and
content-mutating and unrecognised ioctls are all refused on it, as is
mutation or removal of the signature attribute.

**A binary the kernel execs on its own behalf carries at least
PeiosTcb trust.** Where the kernel spawns a userspace helper for its
own purposes rather than at a process's request — resolving a module
name, and any comparable kernel-initiated exec — the implementation
MUST refuse the exec unless the binary's `pip_trust` is at least the
PeiosTcb level. A party may therefore rely on such a helper being
TCB-signed, and on the exec failing rather than proceeding at a lower
tier.

The refusal MUST apply equally when no tier could be derived at all.
"Could not establish trust" and "is not trusted" reach the same
outcome here, so that the requirement cannot be evaded by preventing
the derivation from running.

## 3.3.5 What may not be relied upon

A party MUST NOT rely on the following.

**Execution is not gated, with one exception.** A bad, tampered or
absent signature costs a binary its tier; it does not prevent
execution. PIP determines trust level, not permission to run.
Permission to run is the file's own security descriptor.

The exception is the kernel-initiated exec described above, where a
tier below PeiosTcb refuses the exec outright. It is confined to that
case for a reason: everywhere else there is a requesting process whose
own authority bounds what the exec can do, so an untrusted binary can
be allowed to run and simply carry no tier. A kernel-initiated exec has
no such process behind it — the kernel is acting on its own behalf, at
its own authority — so there is no lesser authority to fall back to and
nothing to bound the result. A party MUST NOT generalise the exception
to ordinary execs.

**Absence of a tier is not observable as an error.** There is no
diagnostic distinguishing "this binary is unsigned", "this signature
is malformed" and "this key is not in the table". All three produce a
process at type 0 and trust 0 with no indication.

**There is no revocation.** A signed binary later found to be
malicious cannot be invalidated. There is no hash blocklist and no
key-scoped revocation. The remedies are removing the file or replacing
the verifier's key table.

**A tier is not a container boundary.** PIP operates inside the
kernel's trust boundary. Kernel compromise voids it, DMA-capable
hardware bypasses it, and it offers nothing equivalent to
hypervisor-based isolation.

**Library trust is compared, not merely required.** Where a process
enables library signature verification, a library has to dominate the
loading process, so raising a process's tier narrows the set of
libraries it can load. A signer distributing libraries alongside a
high-tier program has to sign them at a tier that dominates it.

**Scripts take their interpreter's tier.** A script executed through a
`#!` line contributes nothing; the tier comes from the interpreter
binary. A signer MUST NOT expect signing a script to affect anything,
and an object owner MUST NOT treat "runs at a high tier" as evidence
that the code being run was signed.

---

# 4.1 Scope and Roles

_Peios / Advanced Peios / PSPK / Registry Source Interface_

> The RSI contract between the kernel's registry subsystem and the userspace sources that store data for it — and what a source is not.

This chapter specifies the **Registry Source Interface (RSI)**: the
protocol between the Peios kernel's registry subsystem and the
userspace processes that store registry data for it.

Two roles participate.

The **kernel** is LCS, the Layered Configuration Subsystem. It owns the
registry namespace, the layer model, access control, watches and
transactions, and it holds no storage of its own. There is one kernel.

The **source** is a userspace process that stores the data for one or
more hives and answers the kernel's requests about it. The source role
is publicly implementable: any process holding the required privilege
MAY register as a source, and the kernel is source-agnostic. A
conforming source is the subject of the requirements in this chapter.

The kernel is the party asking. A source is authoritative for the bytes
it returns and for nothing else.

This chapter covers:

- the character device, how a source attaches to it and is recognised,
  and the source slot lifecycle
- the framing and encoding of requests and responses, and the rules
  under which each may be extended
- every operation the kernel issues, its request payload, its response
  payload, and its meaning
- the status vocabulary a source answers with
- what the kernel validates for itself rather than believing
- the obligations a conforming source MUST satisfy

This chapter does not cover:

- The registry data model — hives, keys, path entries, values, layers,
  tombstones, resolution — which is described in the Peios Kernel TRM.
  A source does not need it.
- The system-call and ioctl surface the registry offers to ordinary
  programs, which is that subsystem's own documentation.
- The registry backup format, which is specified in its own chapter.
- How a source stores its data, serves concurrent requests, or computes
  its answers.

## 4.1.1 What a source is not

A source stores and returns. It MUST NOT resolve layers, filter results
by visibility, evaluate a Security Descriptor, interpret a path beyond
the parent and child names it is given, or dispatch a notification.
Every such decision belongs to the kernel, and a source that made one
would be making it with less information than the kernel has.

A source never learns the identity of the process on whose behalf a
request was issued. Requests carry no caller identity, and there is no
mechanism by which a source could obtain one.

## 4.1.2 What the kernel establishes for itself

The kernel validates that every response is structurally well-formed,
that names are valid, that Security Descriptors parse and satisfy the
mask rules, that sequence numbers cannot be from the future, and that
metadata blocks cover exactly the GUIDs they should. §4.5 lists these.

It cannot validate meaning. A source is inside the trusted computing
base precisely because the kernel has no independent copy of what a
source returns: a Security Descriptor granting everyone full access to
a sensitive key is a valid Security Descriptor, and the kernel will
enforce it.

A source is trusted with the correctness of the registry's access
control for the hives it backs. That is the whole trust model, and it
is why attaching requires the highest privilege the system has.

---

# 4.2 The Channel

_Peios / Advanced Peios / PSPK / Registry Source Interface_

> Attaching to /dev/pkm_registry — the privileges the open handler demands, registration, source slots, and resuming a Down slot.

## 4.2.1 The device

A source attaches by opening the character device `/dev/pkm_registry`.

The `open()` handler evaluates the calling thread's effective token.
The caller MUST hold `SeTcbPrivilege` and it MUST be **enabled**, not
merely present; `open()` fails `EPERM` otherwise. An unprivileged
process cannot obtain a descriptor to the device at all.

One open descriptor corresponds to one source connection.

## 4.2.2 Registration

Before entering the request loop a source MUST register its hives, by
issuing the `REG_SRC_REGISTER` ioctl on the device fd. The argument is
a `reg_src_register_args`:

| Field | Type | Description |
|---|---|---|
| `hive_count` | `u32` | Number of hive entries. MUST be non-zero. |
| `_pad` | `u32` | Reserved. MUST be zero. |
| `max_sequence` | `u64` | The highest sequence number persisted anywhere in this source's storage. A single value for the whole source, not per hive. |
| `hives_ptr` | `u64` | Userspace address of an array of `hive_count` `reg_src_hive_entry` structures. |

Each `reg_src_hive_entry`:

| Field | Type | Description |
|---|---|---|
| `name_len` | `u32` | Length of the hive name in UTF-8 bytes. |
| `_pad0` | `u32` | Reserved. MUST be zero. |
| `name_ptr` | `u64` | Userspace address of the hive name. Not null-terminated. |
| `root_guid` | `u8[16]` | The GUID of this hive's root key. MUST NOT be all-zero. |
| `flags` | `u32` | `RSI_HIVE_PRIVATE` (`0x01`). All other bits reserved and MUST be zero. |
| `_pad1` | `u32` | Reserved. MUST be zero. |
| `scope_guid` | `u8[16]` | The private scope identifier. MUST be all-zero unless `RSI_HIVE_PRIVATE` is set. |

Each hive carries its own name pointer; there is no separate array of
names.

The kernel validates, and registration fails if any of the following
does not hold:

- every hive name is valid — UTF-8, no null byte, no separator,
  non-empty, within the configured component length;
- no hive name is `CurrentUser` in any casing, which is reserved;
- the route identity of each hive — its case-folded name paired with
  its scope — does not collide with one held by an Active source
  (`EEXIST`);
- no root GUID is all-zero, and the root GUIDs within this request are
  distinct from each other;
- a hive without `RSI_HIVE_PRIVATE` carries an all-zero `scope_guid`;
- `hive_count` is within `MaxHivesPerSource` and the registered source
  count is within `MaxRegisteredSources` (`ENOSPC`);
- `max_sequence` is not `U64_MAX`, since the kernel MUST be able to
  allocate above it (`EOVERFLOW`).

`max_sequence` initialises the kernel's global sequence counter to at
least one above it, so that new writes always outrank anything already
persisted. A source MUST report it accurately; under-reporting it
allows a new write to collide with a stored entry.

## 4.2.3 Source slots

A successful registration creates a **source slot**, the kernel object
owning one connection and its hive set. A slot is Active or Down.

Each registered hive has a stable identity: its case-folded name, its
visibility, its scope GUID if private, and its root GUID.

A source crash or an fd close marks the slot Down. It does **not**
unregister the source or retire any hive identity. Down slots keep
their identities reserved, and collision checks include them. There is
no implicit retirement.

While a slot is Down its hives are unavailable; operations needing a
round trip fail, key descriptors held by processes remain valid, and
watches remain armed.

## 4.2.4 Resuming a Down slot

A new process MAY take over a Down slot. It MUST hold `SeTcbPrivilege`
and it MUST register **exactly the same hive set**: the same number of
hives, and for each of them the same case-folded name, the same
visibility, the same scope GUID and the same root GUID.

Partial resume is rejected. In particular:

- A request whose only mismatch is a different root GUID for an
  otherwise-matching hive fails `ESTALE`.
- Other partial or malformed resume attempts fail `EINVAL`.
- A collision with an **Active** slot fails `EEXIST`, and takes
  precedence over `ESTALE` when both would apply.

A source MUST NOT expect to add hives by resuming a Down slot with a
larger set; that is a partial-resume failure. New hives require a new
slot.

The kernel authenticates a replacement by `SeTcbPrivilege`, not by
process identity. Process identity cannot survive a crash and restart,
so nothing records or compares it.

On a successful resume the slot becomes Active and the kernel replays
any pending layer deletions before resuming normal traffic.

## 4.2.5 Before serving anything

A source MUST purge orphaned key records before completing
registration. An orphaned record is a key with no path entry in any
layer, left behind by a key that was unlinked but not dropped before
the previous shutdown.

If that cleanup cannot be completed, the source MUST fail registration
rather than become Active with known orphans. The kernel does not
verify this and cannot: it has no independent view of the source's
storage.

On first boot against an empty store, a source MUST create a root key
record for each hive it backs, generating a GUID for each and giving it
an appropriate default Security Descriptor, and MUST persist them. The
root GUIDs it then reports in registration are those. Subsequent
startups reuse the persisted ones.

---

# 4.3 Message Framing

_Peios / Advanced Peios / PSPK / Registry Source Interface_

> The 22-byte request header and its response, the binary encoding, the asymmetry in how each side may be extended, and the concurrency rules.

The protocol is binary and multiplexed. All multi-byte integers are
little-endian.

## 4.3.1 The request header

A request is 22 bytes of header followed by an operation-specific
payload.

| Offset | Size | Field |
|---|---|---|
| 0 | 4 | `total_len` |
| 4 | 8 | `request_id` |
| 12 | 2 | `op_code` |
| 14 | 8 | `txn_id` |

`total_len` is the whole message including the header.

`request_id` matches a response to its request. Request ids are
allocated by the kernel, are strictly increasing within one connection,
and are never reused while that connection lives — including after a
request has timed out.

`txn_id` is the transaction the operation belongs to, or zero for none.
When non-zero, the source MUST process the operation inside that
transaction's context.

## 4.3.2 The response header

A response is 14 bytes of header followed by a payload. It carries no
`txn_id`.

| Offset | Size | Field |
|---|---|---|
| 0 | 4 | `total_len` |
| 4 | 8 | `request_id` |
| 12 | 2 | `op_code` |

A source MUST copy `request_id` from the request into its response, and
MUST set `op_code` to the request's operation code with the high bit
set — that is, `op_code | 0x8000`. Every operation has a named response
code for this value.

Every response payload begins with a `u32` status at offset 14, so the
minimum response size is 18 bytes.

## 4.3.3 Encoding

A **length-prefixed field** is a `u32` byte count followed by that many
bytes. Strings so encoded are UTF-8 and carry no terminator; a
terminator byte counted in the length is a null byte and is therefore
invalid. Some length-prefixed fields carry binary rather than text —
Security Descriptors and value data — and are not UTF-8.

A **GUID** is 16 raw bytes.

An **array** is a `u32` count followed by that many entries.

A **boolean** is one byte.

## 4.3.4 Extension, and its asymmetry

Requests and responses extend differently, and a source MUST implement
both rules.

A **request** MAY carry trailing fields beyond those a source
recognises. A source MUST skip them, using `total_len` to find the end
of the message. This is how the kernel adds an optional field without
an RSI version bump.

A **response** MUST NOT. The kernel rejects any trailing bytes in a
response payload as malformed data. A source MUST emit exactly the
payload each operation defines, and no more. Extending the protocol in
this direction is done with new operations, never by growing an
existing payload.

## 4.3.5 Reading and writing

The device fd is message-oriented. `total_len` frames messages, but a
successful I/O call never splits or joins one.

**`read()`** returns exactly one complete request. If none is queued, a
blocking read waits until one is, or until the fd is closing, in which
case it returns 0. Under `O_NONBLOCK` an empty queue returns `EAGAIN`.
If the caller's buffer is too small for the next queued request,
`read()` returns `EMSGSIZE` and **does not consume it**.

**`write()`** submits exactly one complete response. The buffer length
MUST equal the response header's `total_len`, and MUST be at least the
response header size. A successful write is never short.

The following all fail `EINVAL` **and tear the connection down**,
marking the source Down:

- a length shorter than the response header;
- a length that does not equal `total_len`;
- an unknown `request_id`;
- a response to a request that has not been delivered;
- a second response to a request already answered;
- an `op_code` that is not the request's with the response bit set;
- a response written on a descriptor that is not the slot's active one.

A source that cannot frame its own messages correctly is not one whose
other answers can be relied on. Ordinary per-operation errors are
reported through the status vocabulary and do not tear anything down.

**`poll()`** reports the fd readable when at least one complete request
is queued, writable while the slot is Active, and `POLLHUP | POLLERR`
when the slot is Down or the fd is closing. An fd that is open but not
yet registered reports nothing.

## 4.3.6 Concurrency and timing

A source MAY process requests in any order and MUST handle multiple
in-flight requests without head-of-line blocking. Responses are matched
by `request_id`, not by arrival order.

The kernel bounds in-flight requests per source by
`MaxConcurrentRSIRequests`, default 256.

A source MUST respond to **every** request it has read, exactly once,
even if the kernel-side caller has already given up. The kernel applies
a request timeout — `RequestTimeoutMs`, default 30 seconds, measured
from the moment it first tries to reserve an in-flight slot and
covering the whole wait — and a source is **not** disconnected for
exceeding it. Late responses are validated and processed exactly like
on-time ones.

A timed-out request remains in the kernel's in-flight table, and keeps
occupying one of the source's slots, until the source answers or the
connection is torn down. A source that accumulates unanswered requests
will exhaust its own concurrency budget.

---

# 4.4 Operations

_Peios / Advanced Peios / PSPK / Registry Source Interface_

> All eighteen RSI operations — path, key, value and layer — and the rule that a source returns every layer-qualified entry without pre-filtering.

Eighteen operations. Every one that returns layer-qualified data MUST
return **all** entries across **all** layers: a source MUST NOT
pre-filter, resolve, or omit. The kernel decides what is effective.

Every response payload begins with a `u32` status. Payload fields
below are listed after it, in wire order, with no padding between them.
"Status only" means the payload is the status and nothing else — 18
bytes in total.

| Operation | Code | Response |
|---|---|---|
| `RSI_LOOKUP` | `0x0001` | `0x8001` |
| `RSI_CREATE_ENTRY` | `0x0002` | `0x8002` |
| `RSI_HIDE_ENTRY` | `0x0003` | `0x8003` |
| `RSI_DELETE_ENTRY` | `0x0004` | `0x8004` |
| `RSI_ENUM_CHILDREN` | `0x0005` | `0x8005` |
| `RSI_CREATE_KEY` | `0x0010` | `0x8010` |
| `RSI_READ_KEY` | `0x0011` | `0x8011` |
| `RSI_WRITE_KEY` | `0x0012` | `0x8012` |
| `RSI_DROP_KEY` | `0x0013` | `0x8013` |
| `RSI_QUERY_VALUES` | `0x0020` | `0x8020` |
| `RSI_SET_VALUE` | `0x0021` | `0x8021` |
| `RSI_DELETE_VALUE_ENTRY` | `0x0022` | `0x8022` |
| `RSI_SET_BLANKET_TOMBSTONE` | `0x0023` | `0x8023` |
| `RSI_BEGIN_TRANSACTION` | `0x0030` | `0x8030` |
| `RSI_COMMIT_TRANSACTION` | `0x0031` | `0x8031` |
| `RSI_ABORT_TRANSACTION` | `0x0032` | `0x8032` |
| `RSI_FLUSH` | `0x0040` | `0x8040` |
| `RSI_DELETE_LAYER` | `0x0050` | `0x8050` |

Any other operation code is invalid.

## 4.4.1 Path operations

### 4.4.1.1 `RSI_LOOKUP`

Look up a child entry under a parent key. This is the path-walking
primitive; the kernel issues one per path component.

**Request:** `parent_guid` (16), `child_name` (length-prefixed).

**Response:** `entry_count` (`u32`), then that many path entries:

| Size | Field |
|---|---|
| 4+n | `layer_name` |
| 1 | `target_type`: 0 = GUID, 1 = HIDDEN |
| 16 | `target_guid` |
| 8 | `sequence` |

then `metadata_count` (`u32`), then that many key metadata entries:

| Size | Field |
|---|---|
| 16 | `guid` |
| 4+n | `sd`, binary |
| 1 | `volatile` |
| 1 | `symlink` |
| 8 | `last_write_time` |

An empty response — `entry_count` zero — means the child does not exist
in any layer. That is an ordinary answer, not an error.

The metadata block is **deduplicated**: exactly one entry per distinct
GUID referenced by the path entries. A source MUST satisfy all of:

- every GUID appearing as a target has exactly one metadata entry;
- no metadata entry is duplicated;
- no metadata entry is unreferenced;
- no metadata GUID is all-zero;
- a HIDDEN entry MUST carry an all-zero `target_guid` and MUST NOT
  contribute a metadata entry.

Violating any of these is malformed data.

The kernel resolves symlink targets itself, by issuing a separate
`RSI_QUERY_VALUES` for the key's default value. A source MUST NOT
interpret the symlink flag or follow anything.

### 4.4.1.2 `RSI_CREATE_ENTRY`

Create a path entry `(parent, child_name, layer) → guid`.

**Request:** `parent_guid` (16), `child_name`, `layer_name`,
`child_guid` (16), `sequence` (`u64`).

**Response:** status only.

Always paired with `RSI_CREATE_KEY`, which carries the same GUID. The
kernel sends `RSI_CREATE_ENTRY` **first**, so that a losing race is
detected before a key record is created: an `RSI_ALREADY_EXISTS` here
means another writer got the name.

### 4.4.1.3 `RSI_HIDE_ENTRY`

Create a HIDDEN path entry at `(parent, child_name, layer)`.

**Request:** `parent_guid` (16), `child_name`, `layer_name`,
`sequence` (`u64`).

**Response:** status only.

### 4.4.1.4 `RSI_DELETE_ENTRY`

Remove the path entry at `(parent, child_name, layer)`, whether it was
a GUID entry or a HIDDEN one.

**Request:** `parent_guid` (16), `child_name`, `layer_name`.

**Response:** status only.

### 4.4.1.5 `RSI_ENUM_CHILDREN`

Enumerate every child entry under a parent, across all layers.

**Request:** `parent_guid` (16).

**Response:** `child_count` (`u32`), then per child:

| Size | Field |
|---|---|
| 4+n | `child_name` |
| 4 | `entry_count` |
| … | that many path entries, in the `RSI_LOOKUP` entry format |

followed by **one** metadata block, in the `RSI_LOOKUP` metadata
format, covering the distinct GUIDs across all children. Emitting it
once rather than per child is what makes this cheaper than a lookup per
name.

The same deduplication and closure rules apply, evaluated across the
whole response.

## 4.4.2 Key operations

### 4.4.2.1 `RSI_CREATE_KEY`

Create a key record. The GUID is assigned by the kernel.

**Request:** `guid` (16), `name`, `parent_guid` (16), `sd` (binary,
length-prefixed), `volatile` (1), `symlink` (1).

**Response:** status only.

The path entry linking the key into the namespace is created separately
by `RSI_CREATE_ENTRY`. For a symlink key, the target is written
afterwards as a default `REG_LINK` value through `RSI_SET_VALUE`.

A source MUST persist the GUID exactly as given: no rewriting, no
remapping, no reassignment. GUIDs are the kernel's identity for keys
and the source's primary key for storage.

### 4.4.2.2 `RSI_READ_KEY`

**Request:** `guid` (16).

**Response:** `name`, `parent_guid` (16), `sd`, `volatile` (1),
`symlink` (1), `last_write_time` (`i64`).

### 4.4.2.3 `RSI_WRITE_KEY`

Update a key's mutable fields.

**Request:** `guid` (16), `field_mask` (`u32`), then the named fields
in bit order.

| Bit | Field | Encoding |
|---|---|---|
| 0 | `sd` | length-prefixed |
| 1 | `last_write_time` | `i64` |

Only fields whose bit is set are present. Any other bit set in
`field_mask` is invalid.

**Response:** status only.

There is no way to express a change to the GUID, the volatile flag or
the symlink flag: those fields are simply absent from the request. A
source MUST reject a request attempting to modify an immutable field
with `RSI_INVALID`.

A `field_mask` of zero is a well-formed existence check that mutates
nothing.

### 4.4.2.4 `RSI_DROP_KEY`

Purge everything associated with a GUID: the key record, all value
entries across all layers, all path entries, and all blanket
tombstones.

**Request:** `guid` (16).

**Response:** status only.

`RSI_DROP_KEY` MUST be idempotent. If the GUID does not exist — already
purged by startup cleanup, say — the source MUST return `RSI_OK`.

The kernel issues this when the last descriptor to an unnamed key
closes, and it issues it **without a waiting caller**. A source MUST
answer it like any other request.

## 4.4.3 Value operations

### 4.4.3.1 `RSI_QUERY_VALUES`

Retrieve every layer entry for one value, or for all values on a key.

**Request:** `guid` (16), `value_name`, `query_all` (1). When
`query_all` is set the value name is empty and every value on the key
is returned.

**Response:** `entry_count` (`u32`), then per entry:

| Size | Field |
|---|---|
| 4+n | `value_name` |
| 4+n | `layer_name` |
| 4 | `type` |
| 4+n | `data`, binary |
| 8 | `sequence` |

then `blanket_count` (`u32`), then per blanket tombstone:

| Size | Field |
|---|---|
| 4+n | `layer_name` |
| 8 | `sequence` |

The blanket list is part of every response, including one for a single
named value: the kernel needs it to resolve that name.

A tombstone entry carries type `REG_TOMBSTONE` (`0xFFFF`) and
zero-length data. A source MUST NOT return a tombstone with data, an
undefined value type, or data exceeding the configured maximum value
size.

### 4.4.3.2 `RSI_SET_VALUE`

Store a value entry at `(guid, value_name, layer)`, replacing any
existing entry for that triple.

**Request:** `guid` (16), `value_name`, `layer_name`, `type` (`u32`),
`data`, `sequence` (`u64`), `expected_sequence` (`u64`).

**Response:** status only.

**Conditional writes.** A source MUST support `expected_sequence`.
Zero means unconditional. Non-zero means the source MUST **atomically**
verify that the current entry at `(guid, value_name, layer)` carries
that sequence number before writing, and MUST return `RSI_CAS_FAILED`
without writing if it does not match or if no entry exists.

The condition is against the layer's own entry, not against any
resolved value. A source does not know what is effective and MUST NOT
try to work it out.

### 4.4.3.3 `RSI_DELETE_VALUE_ENTRY`

Remove the entry at `(guid, value_name, layer)`, whether it was a value
or a tombstone.

**Request:** `guid` (16), `value_name`, `layer_name`.

**Response:** status only.

This operation MUST be idempotent: a source MUST return `RSI_OK` when
there was no entry to remove. The kernel does not mask `RSI_NOT_FOUND`
here, so a source returning it makes the caller's delete fail.

### 4.4.3.4 `RSI_SET_BLANKET_TOMBSTONE`

Set or remove a blanket tombstone on `(guid, layer)`.

**Request:** `guid` (16), `layer_name`, `set` (1), `sequence` (`u64`).

**Response:** status only.

## 4.4.4 Transaction operations

### 4.4.4.1 `RSI_BEGIN_TRANSACTION`

**Request:** `txn_id` (`u64`), `mode` (`u32`).

The transaction id appears in the payload. The request header's own
`txn_id` is zero for this operation, since the transaction does not yet
exist. `RSI_COMMIT_TRANSACTION` and `RSI_ABORT_TRANSACTION` carry it in
both places.

| Mode | Value | Meaning |
|---|---|---|
| `RSI_TXN_READ_WRITE` | 0 | An ordinary transaction. |
| `RSI_TXN_READ_ONLY` | 1 | A point-in-time read snapshot. |

**Response:** status only.

For `RSI_TXN_READ_WRITE`, reads tagged with the id MUST observe the
transaction's own uncommitted writes, and writes tagged with it MUST be
committed atomically by `RSI_COMMIT_TRANSACTION`.

For `RSI_TXN_READ_ONLY`, reads tagged with the id MUST observe a stable
point-in-time snapshot. The kernel MUST NOT send a mutating operation
with a read-only transaction id, and a source that receives one MUST
reject it with `RSI_INVALID` and MUST NOT mutate anything. A read-only
transaction is released with `RSI_ABORT_TRANSACTION`; the kernel MUST
NOT send `RSI_COMMIT_TRANSACTION` for one.

A source whose store cannot support a mode MAY answer
`RSI_TXN_NOT_SUPPORTED` for that mode. The two are independent: a
source MAY support read-only snapshots without supporting read-write
transactions.

### 4.4.4.2 `RSI_COMMIT_TRANSACTION`

**Request:** `txn_id` (`u64`).

**Response:** status only.

On success every change in the transaction MUST be durable. On failure
every change MUST be rolled back.

### 4.4.4.3 `RSI_ABORT_TRANSACTION`

**Request:** `txn_id` (`u64`).

**Response:** status only.

Sent when a transaction is closed without committing, on timeout, and
to release a read-only snapshot. The kernel MAY send it without a
waiting caller.

## 4.4.5 Layer operations

### 4.4.5.1 `RSI_DELETE_LAYER`

Remove everything tagged with a layer name.

**Request:** `layer_name`.

**Response:** `orphaned_guid_count` (`u32`), then that many GUIDs (16
each).

The source MUST atomically remove all path entries, all value entries
and all blanket tombstones whose layer is `layer_name`, and MUST NOT
remove any key record. Orphan cleanup is the kernel's, through
`RSI_DROP_KEY`.

`orphaned_guids` MUST list exactly the GUIDs that lost their last path
entry as a result, with no nil GUID and no duplicates. The kernel
tracks them for deferred deletion.

An unknown layer name is **not** an error. A source with no entries for
it MUST return `RSI_OK` with an empty orphan list — a layer may have
entries in one source and none in another.

## 4.4.6 Maintenance

### 4.4.6.1 `RSI_FLUSH`

Persist pending writes for one hive to durable storage.

**Request:** `hive_name`.

**Response:** status only, returned when persistence is confirmed.

This is the only operation that identifies its target by hive name
rather than by GUID, because flushing is a hive-level act — a WAL
checkpoint, say — not a key-level one.

---

# 4.5 Conformance

_Peios / Advanced Peios / PSPK / Registry Source Interface_

> The status vocabulary, the obligations not tied to one operation, what the kernel validates, and where the trust boundary falls.

A conforming source MUST satisfy every requirement in this chapter.
This section collects the obligations that are not tied to one
operation, and the status vocabulary.

## 4.5.1 Status codes

Every response payload begins with a `u32` status. Zero is success.

| Status | Code | Kernel maps to | Meaning |
|---|---|---|---|
| `RSI_OK` | 0 | success | The operation completed. |
| `RSI_NOT_FOUND` | 1 | `ENOENT` | A key, value or path entry does not exist. |
| `RSI_ALREADY_EXISTS` | 2 | `EEXIST` | A path entry or key already exists. |
| `RSI_STORAGE_ERROR` | 3 | `EIO` | A failure in the backing store. |
| `RSI_NOT_EMPTY` | 4 | `ENOTEMPTY` | A key still has children or values. |
| `RSI_TOO_LARGE` | 5 | `ENOSPC` | Value data exceeds the maximum size. |
| `RSI_TXN_BUSY` | 6 | `EBUSY` | A transaction could not take the write lock. |
| `RSI_INVALID` | 7 | `EINVAL` | A malformed request or an invalid field value. |
| `RSI_CAS_FAILED` | 8 | `EAGAIN` | A conditional write's sequence did not match. |
| `RSI_TXN_NOT_SUPPORTED` | 9 | `ENOTSUP` | This transaction mode is not supported. |

A source MUST NOT return a code outside this vocabulary. One outside it
is malformed data.

Source-specific detail is never surfaced to the process that made the
registry call. The status is the whole interface, and a source MUST
choose the code that most accurately describes what happened.

## 4.5.2 Obligations

**Respond to everything.** A source MUST send exactly one response for
every request it has read, even after the kernel-side caller has timed
out. A request the source has read and will never answer occupies an
in-flight slot until the connection is torn down.

**Return complete layer data.** When asked for values or path entries,
a source MUST return **all** layer entries. It MUST NOT pre-filter,
resolve, or omit any. Layer resolution is the kernel's.

**Order enumerations deterministically.** The same request against
unchanged hive state MUST return the same ordering every time. This
applies to the child list of `RSI_ENUM_CHILDREN`, the value entries and
the blanket tombstone list of `RSI_QUERY_VALUES`, and the path entries
of `RSI_LOOKUP`. Ascending folded name, then layer, then sequence
satisfies it.

This is correctness, not tidiness. The kernel exposes enumeration to
callers as a dense index walk — position 0, 1, 2 until exhaustion —
observing the source's ordering at each step. A source that returns the
same set in a different order across those observations makes the walk
revisit some entries and never see others, which surfaces as duplicate
and silently missing keys. An unordered SQL query or a hash-map
iteration does **not** satisfy this: `UNION ALL` without `ORDER BY` and
deliberately randomised map iteration both yield different orderings
for identical input.

The obligation constrains ordering **within one response** only. It
does not constrain the order in which concurrent requests are
processed, and it does not by itself make a multi-step enumeration
atomic against concurrent mutation — a caller that needs a stable view
across a whole walk uses a read-only transaction.

**Preserve GUIDs exactly.** A source MUST persist a GUID as given.

**Handle concurrency.** A source MUST handle multiple in-flight
requests without head-of-line blocking, and MAY process them in any
order.

**Serialise commits.** Concurrent read-write commits MUST be
serialised, and a commit MUST be atomic. The kernel does no conflict
detection: it relies on commits being ordered and atomic, and lets the
later write win by sequence number.

**Support conditional writes.** `expected_sequence` on `RSI_SET_VALUE`
MUST be honoured atomically.

**Protect immutable fields.** `RSI_WRITE_KEY` requests attempting to
modify the GUID, the volatile flag or the symlink flag MUST be rejected
with `RSI_INVALID`.

**Create hive roots on first boot**, and **purge orphans before
registering**. Both are described in §4.2.

## 4.5.3 What the kernel validates

A source cannot rely on a malformed response being tolerated. The
kernel checks each of the following, and two categories of failure have
different consequences.

**Malformed data** — a structurally valid message with invalid content
— fails the request with `EIO`, emits an audit event naming the source
and the class of failure, and **leaves the source running**, since
corruption may be localised. The classes are: an unparseable or
mask-invalid Security Descriptor; an invalid layer name, key name or
value name; a payload of the wrong shape or with trailing bytes; a
metadata block that is incomplete, duplicated, unreferenced or nil; an
invalid value type, a tombstone carrying data, or oversized data; a
nil or duplicated orphan GUID; a status code outside the vocabulary;
and the two sequence rules below.

**Malformed protocol** — a structurally invalid message, a framing
error, an unknown or duplicate request id, an operation code that does
not match its request — is treated as a crash. The connection is torn
down and the source is marked Down.

### 4.5.3.1 The two sequence rules

A source MUST NOT return a layer-qualified entry whose sequence number
is greater than or equal to the next number the kernel would allocate.
Sources store the numbers the kernel assigns them and cannot
legitimately hold a future one. Without this rule a compromised source
could fabricate a sequence number and win every resolution tie in its
own hives.

A source MUST NOT return duplicate sequence numbers at the same
precedence where they would have to be compared to select a winner.
The kernel rejects the response rather than choosing arbitrarily.
Duplicates that are never compared are not an error.

## 4.5.4 The trust boundary

A source is inside the trusted computing base. The kernel has no
independent copy of anything a source returns, so a compromised source
controls the access-control outcome for its hives entirely: it can
return a permissive Security Descriptor for any key and the kernel will
enforce it. Structural validation catches malformed data; it cannot
catch data that is well-formed and false.

Three consequences follow that an operator should understand.

A source backing the hive that holds layer metadata can fabricate
precedence and enabled values, and so decide which layer wins every
resolution contest system-wide. The privilege check applied when
precedence is *written* does nothing about a fabricated *read*.

The same source can return permissive descriptors for layer metadata
keys, granting any process write access to any layer.

`SeRestorePrivilege` implies descriptor control: a restore replaces
every Security Descriptor in the subtree it covers.

Sources MUST therefore run with tightly scoped privileges and be
protected by descriptors on their service definitions. The kernel emits
an audit event for every source data validation failure.

---

# 5.1 Scope and Roles

_Peios / Advanced Peios / PSPK / Registry Backup Format_

> The byte stream representing a registry key and everything beneath it with full layer fidelity, its two roles, and the constraints that shaped it.

This chapter specifies the **registry backup format**: the byte stream
that represents a registry key and everything beneath it, with full
layer fidelity.

Two roles participate, and unlike a live protocol they need not exist
at the same time.

The **writer** produces a stream. The Peios kernel's registry subsystem
is one writer; so is any third-party tool that constructs a backup for
migration, provisioning or archival.

The **reader** consumes one. The kernel is a reader, and so is any tool
that inspects, converts or transforms a backup.

The format is specified rather than described because both roles are
publicly implementable, and because a stream outlives the process that
wrote it. A backup taken on one machine is restored on another, by a
different implementation, perhaps years later.

This chapter covers:

- the framing common to every record, and the encoding of its fields
- the versioning fields, and the rules under which the format may be
  extended
- every record type and its payload, in wire order
- the ordering of records within a stream
- the integrity trailer and exactly what it covers
- what a reader MUST validate, and what a restore MUST do with a
  stream it accepts

This chapter does not cover:

- The registry data model the stream represents — hives, keys, layers,
  tombstones, resolution — which is described in the Peios Kernel TRM.
- The Registry Source Interface, which is a separate chapter. The
  backup format is a kernel-level format; a registry source never sees
  one.
- The system calls that produce and consume a stream.
- The binary layout of a Security Descriptor or a SID, which is
  defined in PCDS. A backup carries them as opaque byte strings.

## 5.1.1 Design constraints

The format is shaped by five requirements, and an implementation of
either role has to respect all of them.

**Streamable.** A stream is written to an arbitrary descriptor — a
file, a pipe, a socket — in a single forward pass, and read back the
same way. Neither role may require seeking.

**Full layer fidelity.** Every path entry, value, tombstone and blanket
tombstone carries its layer tag. Restoring reconstructs the layered
state, not a flattened view of it.

**Depth-first pre-order.** A key's parent always appears before it, so
a reader can create keys top-down without buffering a tree.

**Descriptors inline.** Each key record carries its own Security
Descriptor, with no deduplication and no shared table. Redundancy is
left to external compression.

**Self-verifying.** A trailer carries a record count and a
cryptographic checksum, so truncation and corruption are detectable
before anything is acted on.

---

# 5.2 Stream Structure

_Peios / Advanced Peios / PSPK / Registry Backup Format_

> Record framing, field encoding, ordering, and how the format is versioned and extended.

All multi-byte integers in this format are **little-endian**, in the
record framing and in every payload.

## 5.2.1 Record framing

Every record begins with the same six-byte header.

| Offset | Size | Field |
|---|---|---|
| 0 | 2 | `record_type` |
| 2 | 4 | `record_len` |

`record_len` is the record's total size **including** this header, so
its minimum valid value is 6. The payload follows immediately.

A reader MUST validate `record_len >= 6` and MUST verify that the
record body can be read in full before acting on the record or
skipping it.

| Record | Code |
|---|---|
| `HEADER` | `0x01` |
| `LAYER` | `0x02` |
| `KEY` | `0x03` |
| `PATH_ENTRY` | `0x04` |
| `VALUE` | `0x05` |
| `BLANKET_TOMBSTONE` | `0x06` |
| `TRAILER` | `0xFF` |

## 5.2.2 Field encoding

A **length-prefixed field** is a `u32` byte count followed by that many
bytes. Names — hive name, layer name, child name, value name — are
UTF-8. Three length-prefixed fields are **binary, not UTF-8**: a
`LAYER` record's owner SID, a `KEY` record's Security Descriptor, and a
`VALUE` record's data.

A **GUID** is 16 raw bytes. An all-zero GUID is nil and is valid only
where a record's definition says so.

## 5.2.3 Ordering

```
HEADER                         exactly one, first
LAYER                          one per referenced layer, before any key data
  for each key, depth-first pre-order over the merged tree:
    KEY                        the key object
    PATH_ENTRY *               entries owned by this section
    VALUE *                    all layers' values for this key
    BLANKET_TOMBSTONE *        all layers' blankets for this key
TRAILER                        exactly one, last
```

The merged tree is the union of every layer's namespace. Depth-first
pre-order guarantees a key's parent precedes it.

The following are normative:

- `HEADER` MUST be the first record and MUST appear exactly once.
- Every `LAYER` record MUST precede all key data. A `LAYER` record
  after key data has begun is invalid.
- The **root** `KEY` record — the one whose GUID equals
  `HEADER.RootGUID` — MUST be the first `KEY` record in the stream, and
  MUST appear exactly once.
- Within a key's section, records MUST appear in the order
  `PATH_ENTRY`, then `VALUE`, then `BLANKET_TOMBSTONE`. A `PATH_ENTRY`
  after a `VALUE` or `BLANKET_TOMBSTONE` in the same section is
  invalid, as is a `VALUE` after a `BLANKET_TOMBSTONE`.
- No `PATH_ENTRY`, `VALUE` or `BLANKET_TOMBSTONE` may appear before the
  first `KEY` record.
- `TRAILER` MUST be the last record. Any record after it is invalid.

## 5.2.4 Which section a path entry belongs to

A `PATH_ENTRY` naming a key belongs to **that key's** section: it is
one of the incoming entries for the key whose section it is in.

A HIDDEN entry has no key, so it cannot have a section of its own. It
belongs to the section of the key that is its **parent**, alongside
that key's other records. A HIDDEN entry masking a name where no key
exists in any layer is still valid — it expresses that a layer hides a
name, whatever else is or is not there.

A writer MUST NOT emit a GUID-bearing `PATH_ENTRY` in the **root**
key's section. On restore, the target key's existing name is
authoritative and such a record would be discarded. A reader MUST skip
one rather than treat it as an error.

A path entry's parent GUID may belong to a key that only has path
entries in a different layer. The merged-tree walk handles that; it is
not a special case.

## 5.2.5 Versioning

`HEADER` carries two version numbers.

**`FormatVersion`** is the version the stream was written with.

**`MinReaderVersion`** is the oldest reader that can process the stream
correctly. A reader MUST reject a stream whose `MinReaderVersion`
exceeds its own supported version, before acting on any of it.

The current version is 21 in both fields, and readers support 21.

A writer that used only older features SHOULD set a lower
`MinReaderVersion`, so that older readers can restore the stream. A
writer MUST raise `MinReaderVersion` when a new record type is required
for a correct restore, so that an older reader refuses the stream
rather than restoring an incomplete one.

## 5.2.6 Extension

Extension is by **new record types only**.

Unknown record types MAY appear anywhere between `HEADER` and
`TRAILER`. When `MinReaderVersion` permits, a reader MUST skip them,
and MUST treat them as inert: they do not begin or end a key section,
do not satisfy any required record, do not declare a layer, and do not
affect root mapping, sequence remapping or any validation rule. They
**do** count toward `TRAILER.RecordCount` and they **are** covered by
the checksum.

A record payload MUST be consumed **exactly**. Trailing bytes inside a
record of a known type are invalid, even though `record_len` would
accommodate them. A reader MUST NOT skip unrecognised trailing data
within a known record, and a writer MUST NOT add any.

This is deliberately the opposite of the RSI's request convention. A
stream is replayed into mutations long after it was written, and a
field silently ignored there is data silently lost.

---

# 5.3 Records

_Peios / Advanced Peios / PSPK / Registry Backup Format_

> Every record type in wire order — HEADER, LAYER, KEY, PATH_ENTRY, VALUE, BLANKET_TOMBSTONE and TRAILER.

Payload fields are listed in wire order, immediately after the six-byte
framing header, with no padding between them.

## 5.3.1 `HEADER` — `0x01`

Exactly one, first in the stream. Fixed portion 44 bytes plus the hive
name.

| Size | Field | Description |
|---|---|---|
| 8 | `Magic` | The ASCII bytes `PEIOSREG` — `50 45 49 4F 53 52 45 47`. |
| 4 | `FormatVersion` | `u32`. |
| 4 | `MinReaderVersion` | `u32`. |
| 8 | `Timestamp` | `i64`, Unix nanoseconds. |
| 16 | `RootGUID` | The GUID of the key at the root of this backup. |
| 4+n | `HiveName` | The hive the backup was taken from. |

A reader MUST reject a stream whose magic does not match, and MUST
reject one whose `MinReaderVersion` exceeds its own supported version.

`HiveName` MUST be a valid hive name under the ordinary naming rules.

`RootGUID` is a **stream-local** identity for the backup root. On
restore it is remapped to the target key (§5.4).

## 5.3.2 `LAYER` — `0x02`

One per layer name that has layer-tagged data anywhere in the stream.
All `LAYER` records precede all key data.

| Size | Field | Description |
|---|---|---|
| 4+n | `Name` | The layer name. |
| 4 | `Precedence` | `u32`, as observed at backup time. |
| 1 | `Enabled` | `u8`. MUST be 0 or 1. |
| 4+n | `Owner` | Binary SID, as observed at backup time. |

A `LAYER` record is a **stream manifest entry**, not a backup of the
layer's definition. It records what the layer looked like when the
backup was taken so that a restore can validate the stream against it,
and it creates, updates, deletes, enables, disables and authorises
nothing.

A layer's definition is backed up only when its metadata subtree is
itself inside the exported subtree, in which case it appears as
ordinary `KEY`, `PATH_ENTRY` and `VALUE` records like anything else.

A reader MUST validate that every layer name is valid, that folded
layer identities are unique within the manifest, that `Enabled` is 0 or
1, that `Owner` parses as a SID, and that **every** layer name
appearing in a `PATH_ENTRY`, `VALUE` or `BLANKET_TOMBSTONE` has exactly
one corresponding `LAYER` record.

## 5.3.3 `KEY` — `0x03`

One per distinct key object in the subtree, however many layers name
it.

| Size | Field | Description |
|---|---|---|
| 16 | `GUID` | The key's identity. MUST NOT be nil. |
| 4 | `Flags` | `u32`. Bit 0 volatile, bit 1 symlink. |
| 4 | `SDLength` | `u32`. |
| n | `SD` | The full Security Descriptor. |
| 8 | `LastWriteTime` | `i64`, Unix nanoseconds. |

Name and parent GUID are **absent**. They are derivable from the
`PATH_ENTRY` records in the key's own section, and carrying them
separately would let a stream contradict itself.

Undefined bits in `Flags` MUST be zero. A reader MUST reject a record
with any bit outside `0x03` set, rather than ignoring it.

`SD` MUST parse as a Security Descriptor and MUST have an owner.

## 5.3.4 `PATH_ENTRY` — `0x04`

One per name-to-key mapping per layer.

| Size | Field | Description |
|---|---|---|
| 16 | `ParentGUID` | The parent key. MUST NOT be nil. |
| 4+n | `ChildName` | The name under that parent. |
| 16 | `ChildGUID` | The key being named, or an **all-zero GUID** meaning HIDDEN. |
| 4+n | `LayerName` | The layer this entry belongs to. |
| 8 | `Sequence` | `u64`. |

`ChildGUID` is the only GUID field in the format that may be nil, and a
nil one means HIDDEN rather than "no key". No `KEY` record is emitted
for the zero GUID.

## 5.3.5 `VALUE` — `0x05`

One per value entry per layer, tombstones included.

| Size | Field | Description |
|---|---|---|
| 16 | `KeyGUID` | The key this value belongs to. MUST NOT be nil. |
| 4+n | `Name` | The value name; empty for the default value. |
| 4 | `Type` | `u32`. |
| 4 | `DataLength` | `u32`. |
| n | `Data` | The value's bytes. |
| 4+n | `LayerName` | The layer this entry belongs to. |
| 8 | `Sequence` | `u64`. |

`Type` MUST be one of the defined registry value types, or
`REG_TOMBSTONE` (`0xFFFF`). A tombstone MUST carry zero-length data.

## 5.3.6 `BLANKET_TOMBSTONE` — `0x06`

One per blanket tombstone per layer.

| Size | Field | Description |
|---|---|---|
| 16 | `KeyGUID` | The key this blanket belongs to. MUST NOT be nil. |
| 4+n | `LayerName` | The layer. |
| 8 | `Sequence` | `u64`. |

## 5.3.7 `TRAILER` — `0xFF`

Exactly one, last. Payload 40 bytes, so the whole record is 46.

| Size | Field | Description |
|---|---|---|
| 8 | `RecordCount` | `u64`. Every record in the stream, `HEADER` and `TRAILER` included. |
| 32 | `Checksum` | SHA-256. |

`RecordCount` MUST be at least 2 — a stream has at minimum a header and
a trailer.

### 5.3.7.1 What the checksum covers

The checksum is a SHA-256 over the bytes from the **start of the
`HEADER` record's framing header** through the **end of
`TRAILER.RecordCount`**, inclusive.

That is: every byte of every preceding record, then the trailer's own
six-byte framing header, then the eight bytes of `RecordCount`. The 32
checksum bytes themselves are not covered, and nothing follows them.

Skipped unknown records are covered, in full, like any other.

A reader MUST verify both `RecordCount` and `Checksum`. A stream whose
record count does not match, or whose checksum does not verify, MUST be
rejected.

---

# 5.4 Restoring

_Peios / Advanced Peios / PSPK / Registry Backup Format_

> Restoring is a replace rather than a merge — root remapping, GUID rules, parent validation, sequence remapping, and how layers survive.

Restoring is a **replace**, not a merge. The target key's contents and
descendants are removed before the stream's contents are written.

The whole operation — teardown and rebuild together — MUST be atomic.
There is no partial-restore mode, and a store that cannot offer
atomicity cannot be a restore target.

## 5.4.1 The target key survives

A restore is performed against a key that already exists. That key
**object** is not replaced: its GUID, its parent, its name, its
volatile flag and its symlink flag remain what they were and MUST NOT
be taken from the stream.

What the stream's root `KEY` record supplies is the mutable part — the
Security Descriptor and the last write time — which MUST be written to
the target inside the restore.

The root record's immutable flags MUST **match** the target's. A backup
of a volatile key restored onto a non-volatile one, or a symlink onto a
non-symlink, MUST be rejected.

## 5.4.2 Root remapping

`HEADER.RootGUID` is stream-local. Every reference to it — a
`PATH_ENTRY`'s `ParentGUID` or `ChildGUID`, a `VALUE` or
`BLANKET_TOMBSTONE`'s `KeyGUID` — MUST be remapped to the target key's
existing GUID **before** any validation of parent references and before
any record is applied.

The backup root GUID MUST NOT be created as a new key record.

Descendant `KEY` records keep their backup GUIDs, which are written
into the target verbatim.

## 5.4.3 GUID rules

- The stream MUST contain exactly one `KEY` record whose GUID equals
  `HEADER.RootGUID`, and it MUST be the first `KEY` record.
- A non-root GUID MUST NOT appear twice in the stream.
- A non-root GUID MUST NOT equal the restore target's GUID.
- A non-root GUID that already exists **outside** the subtree being
  replaced is a collision and the restore MUST fail. A reader is not
  required to detect this before beginning; it MAY surface during
  replay, in which case the atomicity requirement ensures nothing is
  left behind.

## 5.4.4 Parent validation

Before a path entry is applied, its `ParentGUID` — after root remapping
— MUST be either the restore target's GUID or the GUID of a non-root
`KEY` record **already processed** earlier in the stream. A parent
outside the stream's remapped key set MUST cause the restore to fail.

This is what prevents a crafted backup from injecting path entries into
arbitrary parts of the existing namespace, outside the subtree being
replaced. It is not optional.

A HIDDEN `PATH_ENTRY` is held to a stricter rule: its remapped
`ParentGUID` MUST equal the GUID of the section it appears in, not
merely some already-processed key.

## 5.4.5 Creating a key

A `KEY` record carries no name and no parent, so both come from the
section's path entries.

The **anchor** is the first GUID-bearing `PATH_ENTRY` in the section,
in stream order, whose remapped `ChildGUID` equals the `KEY` record's
GUID. Its remapped `ParentGUID` and its `ChildName` are the parent and
name the key is created with.

- If the section contains no GUID-bearing path entry targeting the
  `KEY` record's GUID, the restore MUST fail.
- If any GUID-bearing `PATH_ENTRY` in a non-root section targets a
  *different* GUID after remapping, the restore MUST fail.
- HIDDEN entries do not satisfy the anchor requirement. They are
  parent-owned records for the key being created and are replayed only
  after it exists.

The key's `LastWriteTime` MUST be written immediately after it is
created, before any of the section's other records are replayed.

Path entries for the **root** section are handled differently:
GUID-bearing ones are not restored, because the target key's existing
incoming path entries remain authoritative. HIDDEN entries in the root
section are parent-owned records for the restore root and MUST be
restored, after parent validation.

## 5.4.6 Sequence remapping

The backup's sequence numbers preserve its internal layer-resolution
ordering. A restore is a new mutation, and its entries MUST become
newer than everything already present while keeping that internal
order. A reader MUST NOT write backup sequence numbers through
unchanged.

Remapping MUST preserve streamability: it MUST NOT require a seekable
input or a pre-scan pass. Before the first layer-qualified record is
applied, the reader records an offset — the next sequence number it
would allocate — and then, for every restored layer-qualified record:

```
new_sequence = restore_sequence_offset + backup_sequence
```

That needs no lookahead: the running maximum is computed during the
single pass.

The offset is held stable for the duration of the restore, so that no
other sequence-allocating mutation can interleave. Reads are not
blocked by it. A restore containing no layer-qualified record at all
need not reserve one.

If a remapped value would reach or exceed `U64_MAX`, the restore MUST
fail. The valid remapped range is `[offset, U64_MAX)`; `U64_MAX` itself
is never a valid sequence number.

When the restore reaches any terminal state — success, abort, failure
or cancellation — the global counter MUST be advanced past the highest
number the restore dispatched, and that advance MUST NOT be rolled
back. Numbers a failed restore dispatched become unused gaps, exactly
like those of any other failed write.

Dispatch order remains structural stream order. It is the remapped
numbers, not the order they were sent in, that preserve the backup's
internal layer resolution.

## 5.4.7 Layers in a restored stream

`LAYER` manifest records define nothing (§5.3). If restored entries
reference a layer that is not in the live layer table, and the stream
does not also restore that layer's metadata subtree as ordinary
registry data, those entries become latent unknown-layer entries and
are ignored during resolution until real metadata exists. If the
metadata subtree **is** included, it is restored through the ordinary
path, and those records are what define the layer.

## 5.4.8 Privilege

A restore replaces every Security Descriptor in the subtree it covers,
so the privilege to perform one effectively confers descriptor control
over everything within its reach.

Before any key record is written, a reader MUST check the layer
manifest: if any declared layer has a precedence above 0, **or** any
existing layer with the same folded identity does, the caller MUST hold
the privilege that guards high-precedence layers, or the restore MUST
be aborted before a single byte is written.

Records in the stream that create or raise persisted layer metadata
above precedence 0 are subject to the same check as an ordinary write
would be. Neither check is redundant: the first covers what the
manifest declares, the second covers what the stream actually writes.

## 5.4.9 Validation before mutation

A reader MAY validate the entire stream — including the trailer's
record count and checksum — before applying any of it. Doing so is
stronger than this specification requires, and it means a corrupt
stream is rejected before anything is torn down rather than after. The
cost is memory rather than seeking, since the records must be retained
across the teardown.

A reader that instead validates as it goes MUST still guarantee that a
checksum failure leaves nothing applied, which the atomicity
requirement already demands.
