# Peios Kernel

> The Peios kernel as it is built: the KMES event subsystem, the KACS access control system, the stratafs layered filesystem, and the LCS registry.

---

# 1.1 Overview

_Peios / Advanced Peios / PKM / Introduction_

> What the Peios kernel is — a Linux kernel with a patch series, the PKM module and a uapi header set compiled into it — and what this manual covers.

The Peios kernel is a Linux kernel with Peios compiled into it. Peios
contributes three things to the tree it is built from: a patch series
of around fifty patches against existing kernel files, a new
`security/pkm` subtree, and a new `fs/stratafs` subtree. None of it is
loadable. `CONFIG_SECURITY_PKM` and `CONFIG_STRATAFS_FS` are both
boolean options, so what they build is linked into `vmlinux`; there is
no module to insert and none to unload.

`security/pkm` builds as **PKM**, the Peios Kernel Module — the name
predates the decision to build in, and the subsystem still carries it.
PKM registers as a Linux Security Module, provides the syscalls in the
PKM range, and holds three of the four subsystems described here. The
fourth, stratafs, is an ordinary in-tree filesystem that sits beside
PKM rather than inside it, and reaches PKM through a kernel-private
header that exports no symbols to modules.

The four subsystems are peers.

**KMES**, the Kernel Mediated Event Subsystem, is the sole event
emission path in Peios. Kernel subsystems and userspace processes alike
emit events exclusively through KMES; it stamps each event with trusted
metadata, buffers it in per-CPU ring buffers, and delivers it to
userspace consumers through shared memory.

**KACS**, the Kernel Access Control System, is the security core: SIDs,
tokens, security descriptors, privileges, impersonation, process
protection, binary signature verification, and the AccessCheck
algorithm that ties them together. KACS also projects Peios identities
onto Linux credentials so that unmodified Linux subsystems make
decisions consistent with Peios policy.

**stratafs** is the layered filesystem. It composes an ordered stack of
strata — ordinary directories, independently owned — into a single
mounted tree, resolving each name in the highest-precedence stratum
that holds it, routing each modification to the stratum that will
accept it, and copying an object up into the designated create stratum
when its own stratum will not. It stores nothing of its own, and
delegates every access decision to KACS.

**LCS**, the Layered Configuration Subsystem, is the kernel half of the
Peios registry. It owns the data model — hives, keys, values, and the
precedence-ordered layers the name refers to — along with access
control, watches, transactions, and the syscall and ioctl surface
through which processes read and write configuration. It holds no
storage of its own, delegating that to userspace sources over the
Registry Source Interface.

This manual describes each subsystem in its own chapter, in the order
above. They are peers, but not independent: KMES stamps
events with identities it obtains from KACS, KACS emits its audit
trail through KMES, LCS enforces access with KACS security
descriptors, and stratafs consults KACS for every delegation decision.
Cross-references between chapters mark these seams.

## 1.1.1 What this manual is

This is a technical reference manual: an exhaustive description of the
kernel as it is actually built. It documents observed behaviour —
formats, algorithms, limits, failure modes — in plain indicative prose.
It is not a standard, and it makes no conformance demands.

The contracts the kernel shares with other parties are specified
elsewhere, in the Peios Core Specification Anthology: the binary
structures that cross subsystem boundaries (GUIDs, SIDs, security
descriptors) in PCDS, and the protocols the kernel speaks with
userspace services and the formats they exchange — the registry source
interface, the registry backup format, the event stream consumer
protocol — in PSPK. Where a chapter touches one of those
contracts, it references the specification rather than restating it,
and describes only what this kernel adds: how the contract is
implemented, and the behaviour on this side of the boundary.

Constants, ABI tables, and catalogues are collected in appendices at
the end of the chapter they belong to, so that a chapter's reference
material sits beside the prose that explains it.

---

# 2.1 Overview

_Peios / Advanced Peios / PKM / KMES_

> KMES is the sole event emission path in Peios, used by kernel subsystems and userspace alike, and the terms this chapter uses for it.

The Kernel Mediated Event Subsystem is the sole event emission path in
Peios. Kernel subsystems and userspace processes alike emit events
exclusively through KMES — there is no alternative path. KMES stamps
each event with trusted metadata at emission time, buffers it in
per-CPU shared memory ring buffers, and delivers it to userspace
consumers that map those buffers directly. It does not persist, index,
or query events, and it imposes no schema or naming convention on them;
those are consumer concerns, handled by eventd.

KMES serves a similar role to ETW in the Windows kernel and auditd in
Linux, but is compatible with neither at the wire, format, or API
level. The design — structured events with a fixed binary header and a
msgpack payload, shared memory delivery, one emission path for kernel
and userspace — was chosen for unified observability with
kernel-trusted metadata.

The consumer-facing contract — the event header layout, the mapped
ring buffer regions, and the protocols a consumer follows to drain,
sleep, and survive buffer swaps — is specified in PSPK's KMES event
stream chapter. This chapter describes the kernel side: how events are
constructed and stamped (§2.2), the in-kernel emission API (§2.3), the
syscall surface (§2.4), how the ring buffers are organised and written
(§2.5), self-configuration through the registry (§2.6), and behaviour
under failure (§2.7).

## 2.1.1 Terminology

An **event** is an indivisible record: a packed binary **header**
carrying KMES-intrinsic metadata, followed by a msgpack-encoded
**payload** whose structure is defined by the emitter. Header and
payload are produced, stored, and consumed together as one contiguous
byte sequence; neither is meaningful alone. KMES treats the payload as
opaque.

The **stamp** fields are the header fields KMES populates itself at
emission time: the timestamp, sequence number, CPU identifier, and
origin class, plus three identity GUIDs captured from KACS — the
**effective token GUID** (the token governing the emitting thread's
access rights, which is the impersonation token when the thread is
impersonating), the **true token GUID** (the process's primary token,
regardless of impersonation), and the **process GUID** (assigned by
KACS at fork and unchanged across exec). The **null GUID** — sixteen
zero bytes — stamps an identity field whose value is unavailable.

The **sequence number** is a per-CPU, per-boot monotonic 64-bit
counter. Each CPU counts independently; the counter starts at zero
when PKM loads and is incremented before its value is taken, so the
first event on each CPU carries sequence number 1 and sequence 0 is
never assigned. A gap in one CPU's sequence indicates lost events. The
pair (`cpu_id`, `sequence`) uniquely identifies an event within a
boot; there is no global sequence.

The **origin class** is a header byte identifying the emission path:
userspace (0), KMES itself (1), KACS (2), or LCS (3). Values 4–255 are
unassigned.

The **event type** is a length-prefixed UTF-8 string in the header
identifying the kind of event. KMES imposes no structure on it and
compares nothing against it; types are consumer vocabulary.

A **ring buffer** is a per-CPU shared memory region — producer
metadata page, consumer metadata page, and a data region — created
and managed by KMES and mapped by consumers. A **consumer** is a
userspace process that maps one or more ring buffers and drains events
from them, typically with one thread per CPU. **Boot-time ring
buffers** are the ordinary per-CPU buffers created at module load
using compiled-in defaults; they are the live consumer-facing buffers
from the first instant, not a separate class.

---

# 2.2 Event Model

_Peios / Advanced Peios / PKM / KMES_

> What an event is on the wire — the packed header, the stamps the kernel applies, ordering, the msgpack payload and the size limits.

## 2.2.1 Structure

An event is a packed binary header followed immediately by its msgpack
payload, written and delivered as a single contiguous byte sequence
with no padding or alignment gaps anywhere. The header is never
represented as a C struct in the kernel; it is serialised field by
field, in order, directly into the ring buffer. The field-by-field
layout — offsets, sizes, and endianness — is part of the consumer
contract and is defined in the PSPK event stream specification. In
summary: all fields before the event type string sit at fixed offsets,
the event type string begins at offset 77 with its `u16` length at
offset 75, the header is exactly `77 + type_len` bytes, the payload
occupies the bytes from `header_size` to `event_size`, and the next
event begins at offset `event_size` from the start of the current one.
All multi-byte header integers are little-endian; the identity GUIDs
are copied as opaque 16-byte values.

`event_size`, `header_size`, and `type_len` are structural fields
computed by KMES during construction. The emitter supplies only the
event type string and the payload, and KMES copies both verbatim.

## 2.2.2 Intrinsic stamps

KMES populates four intrinsic stamp fields at emission time,
unconditionally; the emitter cannot supply them.

- **`timestamp`** — wall clock time (`CLOCK_REALTIME`, via
  `ktime_get_real_ns()`) at the moment KMES accepts the event, in
  nanoseconds since the Unix epoch.
- **`sequence`** — the emitting CPU's per-boot counter, incremented
  and then read, so the first event on each CPU gets 1.
- **`cpu_id`** — the CPU on which the ring buffer write occurs, which
  identifies the per-CPU buffer holding the event.
- **`origin_class`** — for syscall emission, set unconditionally to 0
  (userspace); the caller cannot influence it. For kernel emission,
  the value the calling subsystem passed, written to the header
  without validation — kernel emitters are trusted to pass an
  assigned value.

The timestamp is captured before the sequence number is assigned, so
two events with the same timestamp on the same CPU are ordered by
sequence.

## 2.2.3 Identity stamps

The three identity GUIDs are captured by calling KACS accessors during
the preemption-disabled ring buffer write phase, after the sequence
number is taken and before the header is built. All three accessors
are safe with preemption disabled — each is a handful of pointer
dereferences and a 16-byte copy, with no allocation and no sleeping.
The stamps therefore reflect the thread's identity at the moment of
the ring buffer write, not at syscall entry.

- `kacs_effective_token_guid()` reads the thread's *subjective*
  credentials (`current_cred()`), so during impersonation — which
  installs the impersonation token via `override_creds()` — it yields
  the impersonation token's GUID. Otherwise it equals the true token
  GUID.
- `kacs_primary_token_guid()` reads the *real* credentials
  (`current_real_cred()`), which impersonation leaves untouched, so it
  always yields the process's primary token GUID.
- `kacs_process_guid()` reads the process GUID KACS assigned when the
  process's security state was created at fork. Threads created with
  `CLONE_THREAD` share the process state, and no exec path writes the
  field, so the GUID is stable for the process's lifetime.

All three accessors return the null GUID when there is no task context
(`!current` or not `in_task()`), and the token accessors also return it
when the token cannot be resolved. A fully null identity triple
therefore indicates emission before KACS initialisation, from a kernel
thread, or from interrupt or softirq context. No event is ever stamped
with the identity of a task that merely happened to be interrupted.

For batch emission, the timestamp and the three identity GUIDs are
captured once, before the per-event loop, and shared by every event in
the batch: the batch executes with preemption disabled on one CPU, so
the emitting thread's identity cannot change mid-batch. Each event
still receives its own sequence number.

## 2.2.4 Ordering

Cross-CPU ordering is by `timestamp`. Events with identical timestamps
from different CPUs were genuinely concurrent and have no defined
relative order. Within one CPU, `sequence` is the reliable ordering
primitive, monotonic even across wall-clock discontinuities; events
with identical timestamps on the same CPU are ordered by sequence.

## 2.2.5 Payload

The payload is a single msgpack value. KMES neither interprets nor
modifies it — buffering and delivery are content-blind — and it
performs no payload validation at all for kernel emitters, which are
trusted callers inside PKM.

Payloads arriving through the syscall interface are validated before
acceptance by an iterative (non-recursive) msgpack walker implemented
in Rust, operating on the kernel's staged copy of the payload:

- The payload is exactly one well-formed top-level msgpack value.
  Trailing bytes after it are rejected, as is the never-used `0xc1`
  type byte. A zero-length payload is rejected — an empty byte
  sequence is not a msgpack value — so syscall events always carry a
  payload. (Kernel emitters can emit header-only events with
  `event_size == header_size`.)
- Nesting depth is bounded by the configured `MaxNestingDepth`
  (§2.6). Depth counts from 1 at the top-level value; each child of an
  array or map sits one deeper than its container. A non-empty
  container at the maximum depth is invalid, because its children
  would exceed the limit; an empty array or map at the maximum depth
  is valid, consuming no depth. Map keys and values each occupy a
  child slot, so a map contributes twice its entry count of children.
- The walker's own stack is 256 frames, matching the upper bound of
  the `MaxNestingDepth` range; a configured depth outside 1–256 causes
  every payload to be rejected rather than any to be waved through.
- Length prefixes inside msgpack are big-endian, per the MessagePack
  specification — the one big-endian ingredient in an otherwise
  little-endian event.

A rejected payload fails the syscall with `EINVAL` and nothing is
written to the ring buffer.

The event type string is validated as UTF-8 on the syscall path by the
same staged-copy pass; kernel emitters' type strings are trusted and
copied as given. Types are compared by consumers as raw bytes — no
case folding or normalisation is applied anywhere.

## 2.2.6 Size limits

Three structural bounds apply to every event, and one configurable
policy bound applies to syscall emitters only:

- The event type length fits the header's `u16` `type_len` field and
  is nonzero. Syscall emitters cannot express an overlong type — the
  ABI length field is already `u16` — but the kernel emission API
  takes a `size_t` and enforces the bound itself.
- The total event size (header + payload) fits a `u32`, checked with
  overflow-safe arithmetic on the declared lengths.
- The total event size does not exceed 50% of the per-CPU ring buffer
  capacity — a fixed ratio, not configurable, protecting a CPU's
  event history from a single giant event. An event of exactly half
  the capacity is accepted. Capacity is always a power of two, so the
  halving is exact.
- Syscall events are additionally bounded by the registry-configurable
  `MaxEventSize` (§2.6), rejected with `ENOSPC` when exceeded. Kernel
  emitters are exempt from this policy limit.

---

# 2.3 Emission API

_Peios / Advanced Peios / PKM / KMES_

> The in-kernel emission interface — single and batch emission, the structural checks that drop an event, and what happens when a ring is full.

The emission API is the internal kernel interface through which PKM
subsystems emit events — an ordinary function call inside the module,
not a syscall. Userspace emission goes through the syscall interface
(§2.4). There are two entry points: single emission
(`pkm_kmes_emit_kernel`) and batch emission
(`pkm_kmes_emit_kernel_batch`). Both return nothing: kernel emission
is fire-and-forget, and the emitting subsystem is never notified of a
drop.

## 2.3.1 Single emission

A kernel emitter passes an origin class, an event type (pointer and
length), and a payload (pointer and length). It does not choose a CPU
or buffer: KMES writes the event to the ring buffer of the CPU the
calling code is executing on.

The entire emission path runs with preemption disabled — from before
the current CPU is determined until after the ring buffer write —
which guarantees the emitting thread cannot migrate mid-write and
preserves the single-writer-per-buffer invariant. For kernel emitters
this covers the full path, timestamp capture through ring write; the
payloads are small and trusted, and the non-preemptible window is a
few hundred nanoseconds.

Construction proceeds in order: capture the wall clock timestamp;
increment the CPU's sequence counter and take the new value; capture
the three identity GUIDs from KACS; build the packed header; write
header and payload contiguously into the ring. The ordering
consequences of these steps are described in §2.2.

KMES trusts kernel emitters. It does not validate the origin class,
the type string's encoding, or the payload — only the structural
checks below run. A null event-type pointer is not checked on the
single-emission path; passing one faults.

## 2.3.2 Structural checks and drops

Every kernel emission is checked for: nonzero event type length; type
length within `u16`; total event size within `u32` (overflow-checked);
and total event size within 50% of the per-CPU ring capacity.

A failing event is not written — but its sequence number has already
been consumed, so the drop is visible to consumers as a gap in that
CPU's sequence, and an internal per-CPU dropped-event counter is
incremented. That counter also aggregates events discarded by
overwrite when the buffer wraps; it is not exposed in the ring buffer
metadata and is readable only by the KUnit test harness.

Two further situations discard kernel events entirely outside this
accounting:

- Before KMES initialisation completes, emission is a silent no-op: no
  sequence number is consumed, no counter is incremented, and no gap
  is visible. Events emitted in this window are simply gone.
- If the ring's recorded CPU identity does not match the executing
  CPU, single emission treats it as a structural drop (sequence
  consumed, counter incremented), while both batch paths return early
  without consuming anything.

## 2.3.3 Ring buffer full

Each per-CPU buffer is circular. When it is full, KMES overwrites the
oldest events to make room; the write position advances
unconditionally, and emission never blocks and never fails from buffer
pressure. Consumers detect the overwritten events as sequence gaps,
and a consumer whose read position has been overtaken re-anchors to
the oldest surviving event, as specified in the PSPK event stream
chapter. The overwrite mechanics are described with the write protocol
in §2.5.

## 2.3.4 Batch emission

The batch API emits multiple events in one operation: an origin class
applied to the whole batch, an array of event descriptors (type
pointer and length, payload pointer and length each), and a count.
There is no upper bound on the kernel batch count — unlike the syscall
batch, which caps at 256 entries — so the non-preemptible window is
bounded only by the caller's restraint.

The batch executes as one preemption-disabled section:

1. One wall clock timestamp is captured; every event in the batch
   shares it.
2. The three identity GUIDs are captured once and shared.
3. Each event, in order: structural checks, sequence assignment,
   header build, ring write. The batch structural check is slightly
   stronger than the single-emission one — it also rejects a null
   type pointer, a null payload pointer with a nonzero length, and a
   header size beyond `u32`. A failing event is dropped exactly as in
   single emission (sequence consumed, gap visible, counter
   incremented) and the batch continues with the next event — kernel
   emitters are trusted, and an individual structural failure
   indicates a kernel bug rather than hostile input. This differs
   deliberately from the syscall batch, which stops at the first
   failure so the untrusted caller learns which entry was bad.
4. After the loop, provided at least one event was actually written,
   the new tail position and then the new write position are published
   with release stores — one publication for the whole batch — and
   the consumer wake flag is checked once, incrementing the futex
   counter if a consumer is asleep. A batch in which every event
   failed publishes nothing and performs no wake check.
5. Preemption is re-enabled, and only then is the futex wake syscall
   work performed, outside the non-preemptible window.

Deferring publication gives batch atomicity: consumers observe either
none of the batch or all of it, since the data is fully written before
the single `write_pos` release store. During the batch, the overwrite
check runs against an internal running write offset — the consumer-
visible `write_pos` stays untouched until the end. The tail position
is likewise kept local and published only at the end.

## 2.3.5 Write atomicity

Individual event writes are atomic from the consumer's perspective: an
event's bytes are fully written into the data region before the
`write_pos` release store makes them reachable, so a consumer bounded
by `write_pos` can never observe a partially written event. The
memory-ordering contract this rests on is part of the PSPK event
stream specification; the kernel-side implementation of the write
protocol is described in §2.5.

---

# 2.4 Syscall Interface

_Peios / Advanced Peios / PKM / KMES_

> The three KMES syscalls — emit, batch emit and attach — with their privilege gate, rate limiting, validation and preemption behaviour.

KMES exposes three syscalls in the PKM syscall range (1090–1099):
`kmes_emit` (1090) emits a single event from userspace, `kmes_attach`
(1091) attaches the caller as a consumer of one per-CPU ring buffer,
and `kmes_emit_batch` (1092) emits multiple events in one operation.
All three follow the standard Linux convention — they return −1 and
set errno on failure — and their numbers, entry struct layout, error
tables, and privilege masks are collected in §2.A.

Before KMES initialisation completes, all three syscalls fail with
`ENOMEM`. Before KACS initialisation, the emit syscalls fail closed
with `EPERM`, since privilege checks cannot be performed.

## 2.4.1 kmes_emit

Emits one event. The origin class is set to 0 (userspace)
unconditionally — the caller cannot choose it — and the event is
written to the ring buffer of the CPU the calling thread is executing
on at write time.

### 2.4.1.1 Privilege gate

The caller's effective token has to hold SeAuditPrivilege, enabled;
otherwise the syscall fails with `EPERM`. A successful gate records
SeAuditPrivilege as used on the token, as a KACS standalone privilege
gate; a failed gate records nothing. If recording the used state
itself fails, the syscall also fails with `EPERM`.

### 2.4.1.2 Rate limiting

Callers without enabled SeTcbPrivilege are rate limited per process by
a token bucket: the refill rate and the burst capacity both equal the
configured `MaxEmitRatePerProcess` (§2.6). The bucket is allocated
together with the process's KACS security state at fork, initialised
to full capacity, and freed when the process's security state is
released at exit. Refill is computed against the monotonic clock, so
wall-clock jumps do not affect it. When `MaxEmitRatePerProcess`
changes at runtime, the new rate and capacity take effect immediately
— rates are read live on every operation — and each bucket's current
token count is clamped down to the new capacity.

A token is reserved up front and refunded if the syscall subsequently
fails, so validation failures cost nothing; the refund is clamped to
capacity, which means a refund landing just after a rate decrease can
forfeit the token. An empty bucket fails the reserve with `EAGAIN`,
consuming nothing. Callers holding enabled SeTcbPrivilege bypass the
bucket entirely, and the exemption records SeTcbPrivilege as used.

Rate state is per-process rather than per-SID: per-SID limiting would
penalise unrelated services sharing a SID (LocalService, for
instance). A process that forks to reset its limit is bounded by
`RLIMIT_NPROC`.

### 2.4.1.3 Validation

Validation runs in order and stops at the first failure; the errno
reflects the first failing check.

1. The privilege gate and rate reservation, above.
2. `event_type_len` is nonzero — `EINVAL` otherwise.
3. The declared total event size (`77 + type_len + payload_len`) is
   computed from the length fields alone, without dereferencing either
   userspace pointer, with overflow-checked arithmetic — overflow is
   `EINVAL`.
4. The declared size is within `MaxEventSize` — `ENOSPC` otherwise.
5. The declared size is within 50% of the ring capacity — `ENOSPC`
   otherwise. At this stage the check runs against the first live
   ring's capacity; it is repeated against the actual target CPU's
   ring inside the write phase, so a capacity swap racing the syscall
   can surface `ENOSPC` after all other validation has passed.
6. The event type and payload are copied into a kernel staging buffer
   (`EFAULT` if a pointer is inaccessible, `ENOMEM` if allocation
   fails). Everything after this point — validation and the ring
   write — operates on the kernel copy, closing the TOCTOU window in
   which userspace could rewrite the payload after validation.
7. The event type is validated as UTF-8 — `EINVAL` otherwise.
8. The payload is validated as msgpack within `MaxNestingDepth`
   (§2.2) — `EINVAL` otherwise.

### 2.4.1.4 Preemption

Validation runs with preemption enabled — the userspace copies can
fault, and msgpack validation of a large payload takes microseconds.
Preemption is disabled only around the ring buffer write: determining
the CPU, stamping, writing, publishing `write_pos`, and checking
`need_wake`. The `cpu_id` and identity GUIDs therefore reflect the
thread's state at write time, not at syscall entry. On success the
syscall returns 0 and the event is immediately visible to consumers.

## 2.4.2 kmes_emit_batch

Emits up to 256 events in one call, sharing the privilege check, the
timestamp, the identity capture, and the single `write_pos`
publication across the batch. The 256-entry cap bounds the
preemption-disabled write window to roughly 50–100 microseconds for
typical event sizes.

The caller passes an array of 32-byte entry descriptors (layout in
§2.A; the descriptor padding bytes are documented as
reserved-must-be-zero in the ABI header but are not validated), a
`count`, and an `emitted_out` pointer.

Processing order:

1. The SeAuditPrivilege gate, as for `kmes_emit`.
2. `count` is within 1–256 — `EINVAL` otherwise.
3. `count` tokens are reserved from the rate bucket in one critical
   section, so concurrent threads cannot both pass the check —
   `EAGAIN` if unavailable, and nothing is emitted. SeTcbPrivilege
   exempts as before.
4. Zero is stored to `*emitted_out` before any per-entry work —
   `EFAULT` if unwritable, with nothing emitted.
5. The descriptor array is copied from userspace (`EFAULT`/`ENOMEM`).
6. Each entry, in order, goes through the same staging pipeline as
   `kmes_emit` — declared-size arithmetic, `MaxEventSize`, 50%
   capacity, userspace copy, UTF-8, msgpack. Staging stops at the
   first failing entry.
7. The validated prefix is emitted in one preemption-disabled write
   phase: one timestamp, one identity capture, a sequence number per
   event, origin class 0 throughout, and a single deferred
   publication that makes the whole prefix visible atomically.
8. Unused tokens (`count` minus events emitted) are refunded — only
   events actually emitted are charged.

On full success the syscall returns 0 and writes `count` to
`*emitted_out`. If entry N fails, entries 0 through N−1 are emitted,
N is written to `*emitted_out`, and the syscall returns −1 with the
errno of the failing entry. Failed entries never consume sequence
numbers, so batch validation failures leave no consumer-visible gap.
The final `emitted_out` store is a second write to userspace; if it
faults, the syscall reports `EFAULT` even though the prefix was
already emitted.

Every staged entry is held in kernel memory simultaneously until the
write phase completes, so a batch's transient allocation is bounded by
`count × MaxEventSize` — up to 1 GB at the maximum settings — rather
than by a single event.

## 2.4.3 kmes_attach

Attaches the caller as a consumer of one per-CPU ring buffer,
returning a file descriptor. The consumer contract built on this fd —
the mapped region layout, the drain and notification protocols, and
the re-attach protocol across buffer swaps — is specified in the PSPK
event stream chapter; the TRM side of the mechanics is §2.5.

The caller's effective token has to hold SeSecurityPrivilege, enabled
— `EPERM` otherwise — and a successful gate records SeSecurityPrivilege
as used. `cpu_id` is a logical CPU index using the same numbering as
the ring metadata and event headers.

The slot array is allocated at KMES initialisation and sized by
`nr_cpu_ids`, then filled by walking `for_each_possible_cpu`. Those two
quantities are not the same thing: the array size bounds a valid
`cpu_id`, while the ring count is however many of those slots got a
ring. They agree only when the possible-CPU mask is dense. An index at
or beyond the array size fails with `EINVAL`; so does an index inside
it whose slot holds no live ring.

A consumer learns the array size by calling `kmes_attach` with
`cpu_id` set to `KMES_ATTACH_QUERY_SLOTS` (`0xFFFFFFFF`). The call
takes the same privilege gate, writes the slot count through
`capacity`, returns 0, and opens no descriptor. Enumeration then walks
0 to slots-1 and skips the indexes that answer `EINVAL`.

Counting up until the first `EINVAL` — which is what this interface
used to ask for — is wrong on a sparse mask: it stops at the first
hole, and every ring above it becomes permanently unreachable, filling
and overwriting with no consumer able to attach.

CPUs that were possible but offline at initialisation have rings and
are attachable; hotplug beyond the initial set is not handled (§2.7).

On success the current ring capacity is written to `*capacity` — the
consumer computes its mmap size as `8192 + 2 × capacity` — and the fd
is returned. The fd is opened `O_RDWR | O_CLOEXEC` and supports
exactly two operations: `mmap()` and `close()`. The fd is installed
before the capacity write-back; if that write faults, the fd is closed
again and the syscall returns `EFAULT`, but another thread of the
process can have observed the fd in the interim.

Repeated attaches to the same CPU are permitted and return a new fd
each time; all fds for one CPU share the same ring — producer
metadata, consumer metadata, and data region — so multiple direct
consumers can drain one buffer concurrently, each keeping its own
read position in its own memory. KMES stores no per-consumer state:
the fd's private data is a reference to the ring, nothing more. When
events arrive and `need_wake` is set, KMES increments that buffer's
futex counter and wakes all waiting threads.

---

# 2.5 Ring Buffers

_Peios / Advanced Peios / PKM / KMES_

> One ring per logical CPU — how they are organised, the two producer metadata pages, wrap handling, the write protocol and notification.

## 2.5.1 Organisation

KMES maintains one ring buffer per logical CPU, created for every CPU
in the kernel's possible-CPU set when the module initialises — before
LCS exists, using the compiled-in default capacity — and buffering
events from that first instant. These boot-time buffers are ordinary
buffers: same layout, same overwrite semantics, same generation
model, immediately attachable. If LCS never becomes available they
simply remain the live buffers indefinitely.

Each ring is an independent, reference-counted object holding its
capacity, generation, sequence counter, write and tail positions,
futex counter, dropped-event counter, two metadata pages, and the data
region. There is no shared state between rings on the write path: each
CPU writes only to its own ring, using plain non-atomic fields under
preemption disablement, and the only ordering machinery is a set of
release stores at publication points. Fds taken by consumers hold
references, so a ring — including a superseded generation — survives
until the last consumer releases it.

The data region is a `vzalloc` allocation of exactly `capacity` bytes,
zeroed once at creation and never scrubbed afterwards; overwritten
regions retain stale bytes, which is why the consumer contract forbids
reading beyond an event's `event_size`. Capacity is always a power of
two — enforced at every entry point — so position wrap is a bitwise
AND with `capacity - 1`. The permitted range is 64 KB to 256 MB, with
a 4 MB default.

## 2.5.2 The two producer metadata pages

Producer metadata exists twice. The kernel writes its working copy to
a private page allocated with the ring, and mirrors every store to a
second, consumer-visible page. The consumer-visible page is
shmem-backed and allocated lazily at the first `kmes_attach` for the
ring; shmem backing is what makes the notification futex work, since
a shared (inode-keyed) futex needs a page-backed mapping. Every
producer store — `write_pos`, `tail_pos`, `futex_counter`,
`generation` — is a release store performed to both pages; the static
fields are initialised at ring creation and re-stamped when the shared
page appears. The mmap handler exposes only the shared page. The field
offsets on the page are part of the consumer contract, defined in the
PSPK event stream chapter.

## 2.5.3 Wrap handling

The consumer's data region is double virtual mapped — the same
physical pages appear twice consecutively — so a consumer reads an
event that crosses the physical end of the buffer as one contiguous
byte sequence. The producer side has no such mapping: kernel writes
into the `vzalloc` region are wrap-aware, splitting a byte-range copy
that crosses the boundary into two `memcpy` calls and masking scalar
stores per byte. The contiguity guarantee is a property of the
consumer's view, produced by the mmap layout rather than by the
writer.

## 2.5.4 Write protocol

Each ring has exactly one writer — its CPU — and the write path takes
no locks and performs no cross-CPU atomics. For a single kernel
emission: capture the timestamp; take the next sequence number; if
the event fails a structural check, count the drop and stop (§2.3);
otherwise make room, write the event at `write_pos & (capacity - 1)`,
publish the new `write_pos` (old value plus event size) with a
release store, and check `need_wake`. The release store is what makes
the event atomic from the consumer's side: the bytes are complete
before the position that makes them reachable moves.

Making room is the overwrite walk. While the live span
(`write_pos - tail_pos`) plus the incoming event exceeds capacity,
KMES reads the `event_size` of the event at the tail and advances
`tail_pos` past it, counting each overwritten event in the internal
dropped-event counter, and publishes the advanced tail with a release
store before the new data lands on top of it. Walking the tail is
sequential and possibly cache-cold — the tail can be megabytes from
the write position — a cost accepted in preference to maintaining an
index of event offsets on the hot write path.

The walk carries a corruption guard: if the size field read at the
tail is zero, larger than the capacity, or larger than the live span,
KMES abandons the walk and resynchronises by jumping `tail_pos`
straight to `write_pos`, discarding the entire surviving window in
one step (the discarded span is not itemised in the drop counter) and
emitting a tracepoint.

During batch writes the running write and tail positions are kept in
locals; nothing is published until the batch ends, when the tail and
then the write position get one release store each, followed by a
single `need_wake` check — provided at least one event was written.
Consumers therefore observe a batch atomically, and see one tail
transition per batch rather than one per overwritten event.

## 2.5.5 Notification

The wake path reads the consumer page's `need_wake` byte — the single
consumer-writable byte KMES ever reads, treated as a boolean and
trusted for nothing else. If it is zero, notification costs that one
read. If set, KMES increments the futex counter with a release store
and wakes every thread waiting on it. The futex is a shared,
inode-keyed futex on the shmem producer page at the counter's offset —
a consequence a consumer must match: a `FUTEX_PRIVATE_FLAG` wait on
the mapped address is never woken. Before any consumer has attached,
the shared page does not exist and the wake is skipped entirely,
though the private counter still advances.

The `futex_wake` call itself is issued after preemption is re-enabled,
outside the write window; only the counter increment happens inside
it. Waking a thread that is already awake is a harmless no-op, which
is also why the consumer's relaxed clearing of `need_wake` is safe.

## 2.5.6 Capacity swaps

A valid `BufferCapacity` configuration change replaces every ring.
New rings are allocated first, at the old generation plus one, fully
initialised before any CPU can see them. The switch itself runs under
`stop_machine`: with every CPU quiesced, each ring's surviving events
are migrated to its replacement, the per-CPU live pointers are
switched, and each old ring's published generation is bumped to the
new value — signalling consumers on the old mapping to re-attach. If
an old ring's `need_wake` is set, its futex counter is bumped inside
the quiesced section and the wake is issued after it, so consumers
asleep on a dead generation do not sleep forever.

Migration copies the surviving span in sequence order, re-compacted
contiguously from position zero: the new ring starts with
`tail_pos = 0` and `write_pos` equal to the bytes copied, and the
sequence and dropped-event counters carry over, so sequence numbers
are continuous across a swap. When the new capacity is smaller than
the surviving span, the oldest events are skipped from the tail
forward until the suffix fits — loss is bounded to the oldest prefix,
traced but not counted as drops. Old positions are meaningless in the
new ring; a consumer re-locates by sequence number, per the PSPK
protocol.

If allocating the new rings fails, the old rings stay live at their
size, no generation changes, and the failure is reported through a
`KMES_BUFFER_SWAP_FAILED` event (§2.6). A migration abort — a corrupt
size field encountered inside the quiesced section — abandons the
swap the same way but emits no event. There is no automatic retry;
the next configuration write or reboot tries again. A superseded
generation's pages stay valid for as long as any consumer keeps them
mapped, so during and after a swap old and new rings coexist until
the last old fd closes.

---

# 2.6 Self-Configuration

_Peios / Advanced Peios / PKM / KMES_

> The four parameters KMES reads from the registry, how it bootstraps before LCS exists, and the watch that keeps them current.

KMES reads four operational parameters from the registry under
`Machine\System\KMES\`. Compiled-in defaults carry it from module load
until LCS becomes available; from then on a persistent kernel-internal
watch keeps it current. The key names, types, defaults, and ranges are
in §2.A.

At no point does KMES wait for configuration. The defaults are always
sufficient, and if LCS never appears KMES runs on them indefinitely.

## 2.6.1 Reading and validating

Value names are matched with LCS's value-name comparison rules —
Unicode Simple Case Folding, case-preserving and case-insensitive.
Names in the subtree that do not fold to one of the four canonical
names are unknown keys: they are counted and ignored.

A `REG_DWORD` value carries exactly four little-endian payload bytes
and a `REG_QWORD` exactly eight. A value whose type tag is right but
whose payload length is wrong is not a malformed number — it is
classified as a wrong-type value, and reported as such.

Values are never clamped or silently corrected. A value outside its
range, of the wrong type, of the wrong payload length, absent, or (for
`BufferCapacity`) not a power of two is rejected outright and the
previously active value is retained — the compiled-in default, or the
last accepted value. The registry write itself succeeds, because the
source does not enforce kernel semantics; the registry therefore shows
what was written while the event log shows what KMES is actually
using. Validation happens twice: once when the change plan is built,
and again in C before the plan is applied, so an out-of-range field
reaching the second gate fails the whole application with `EINVAL`.

Applying a plan is all or nothing, and the capacity swap runs first.
A `BufferCapacity` change that cannot be applied therefore also
prevents `MaxEventSize`, `MaxNestingDepth`, and
`MaxEmitRatePerProcess` from being applied in the same pass, even
though those three are valid and would otherwise take effect
immediately for subsequent syscalls. A `MaxEmitRatePerProcess` change
additionally reconfigures every live rate bucket, clamping any bucket
holding more tokens than the new capacity (§2.4).

A valid `BufferCapacity` different from the current one triggers a
ring buffer swap (§2.5).

## 2.6.2 Self-configuration events

KMES reports its own configuration handling through KMES, with origin
class 1. These events are best-effort diagnostics: emission runs
before the configuration is applied and its result is discarded, so a
failed emission neither rolls back a valid application nor activates
an invalid value. Each event's payload is built by a small in-kernel
msgpack writer into a 768-byte buffer; a payload that would exceed it
is silently skipped.

`KMES_SELF_CONFIG_INVALID` reports one missing or invalid value. Its
payload is a msgpack map of exactly nine keys, in order:
`configuration_parent_path` (always `Machine\System\KMES`),
`configuration_name` (the canonical name), `expected_type`,
`expected_min` and `expected_max` (from the key's definition),
`received_kind` (one of `missing`, `wrong_type`, `u32_out_of_range`,
`u64_out_of_range` — a malformed payload length reports
`wrong_type`), `received_type` (the actual registry type code for a
wrong-type value, nil otherwise), `received_value` (the numeric value
for an out-of-range value, nil otherwise), and `retained_value` (the
value KMES continues to use, read before any part of the plan was
applied).

One read reports at most four of these events, which is exactly the
number of configuration keys. A plan that would need more is rejected
before anything is applied, and the entire configuration read is
abandoned.

On a first boot where the KMES key exists but is empty, all four keys
are missing, so the read emits four `KMES_SELF_CONFIG_INVALID` events
and retains all four defaults.

`KMES_BUFFER_SWAP_FAILED` reports a valid `BufferCapacity` change that
could not be applied because replacement rings could not be
allocated. Its payload is a three-key map: `requested_capacity`,
`retained_capacity`, and `errno` — the last carrying the positive
value of `ENOMEM` as an unsigned integer. It is emitted only for
allocation failure; a swap abandoned because migration hit a corrupt
size field produces no event.

## 2.6.3 Bootstrap and watching

1. PKM loads. KMES initialises with compiled-in defaults and creates
   per-CPU rings at the default capacity. They are live immediately.
2. The first Machine-hive source registers, making LCS usable. KMES
   enumerates every value under `Machine\System\KMES\`.
3. Valid values are applied. A `BufferCapacity` differing from the
   current one drives a swap; a matching or absent one changes
   nothing.
4. KMES arms a persistent watch on the key through LCS's internal
   watch mechanism — a kernel-internal registration, not a
   userspace fd-based watch. Delivery is filtered to value-set and
   value-deleted notifications on the key itself, so changes in keys
   below `Machine\System\KMES` do not trigger a re-read.
5. If the key does not exist yet, the fallback watch is armed on the
   Machine hive root and fires on subkey creation at any depth. When
   it fires, KMES re-runs the whole bootstrap: discover the key, read
   it, and re-arm the targeted watch. Deleting the key afterwards
   does not re-arm the fallback.
6. On subsequent changes — administrator edit, or a Group Policy push
   at a higher-precedence layer — the watch fires and KMES re-reads,
   validates, and applies or rejects.

## 2.6.4 Access to the configuration

The configuration keys inherit the Machine hive root security
descriptor, which grants `KEY_ALL_ACCESS` to SYSTEM and
Administrators and `KEY_READ` to Authenticated Users, so unprivileged processes cannot
change KMES's operational parameters. Enforcement is LCS's, not
KMES's — KMES reads values that LCS has already decided the caller
was entitled to write. Domain policy at a higher-precedence layer
provides defence against a compromised local administrator, since
creating a layer above precedence 0 requires SeTcbPrivilege.

The boot-time capacity is the compiled-in default and is not
separately configurable: making it so would need a channel to deliver
a value to the kernel before the registry exists. Once LCS is
available, capacity changes go through the ordinary swap.

---

# 2.7 Failure Modes

_Peios / Advanced Peios / PKM / KMES_

> Ring overrun, event drops, consumer crashes, buffer swap failure, LCS unavailability and clock discontinuity — what each does.

KMES has no external trust boundary on the write path: kernel emitters
are trusted, and userspace emitters are validated at the syscall
boundary (§2.4). Its failure semantics are correspondingly simpler
than a subsystem like LCS that spans the kernel-userspace boundary in
both directions.

## 2.7.1 Ring overrun

When events are emitted faster than consumers drain them, the buffer
fills and KMES overwrites the oldest events. The write path is never
blocked and emission never fails from buffer pressure. Consumers see
the loss as gaps in the per-CPU sequence, and a consumer whose read
position has been overtaken re-anchors to the oldest surviving event.

Overrun is a normal operating condition under load, not an error: the
system degrades by keeping recent events, losing old ones, and telling
consumers that it did.

Two bulk-loss cases sit outside that model. The tail resynchronisation
guard (§2.5) can discard an entire surviving window at once when a
size field reads back implausibly, and a shrinking capacity swap drops
the oldest events that do not fit the new ring. Neither itemises the
discarded events in the drop counter, though both are traced.

## 2.7.2 Event drop

An event is dropped without reaching the buffer when a structural
limit is exceeded — an event type length that cannot be encoded in the
header's `u16` field, or an event larger than half the ring capacity —
and, for syscall emitters only, when the event exceeds `MaxEventSize`
or the payload fails msgpack validation.

The two paths differ in what a consumer sees. For kernel emitters the
sequence number is consumed before the structural checks run, so the
drop appears as a sequence gap; the emitting subsystem is not
notified, since emission is fire-and-forget. For syscall emitters
validation completes before the write phase, so no sequence number is
consumed and no gap appears — the drop is visible only to the caller,
as the syscall's error return.

Events emitted before KMES initialisation completes are discarded with
no sequence consumed and no counter incremented, and are therefore
invisible to consumers in both ways.

The internal per-CPU dropped-event counter aggregates structural drops
and overwrite losses together. It is not exposed in the ring metadata
and is reachable only through the KUnit test interface.

## 2.7.3 Consumer crash

A crashed consumer's mappings are cleaned up by the ordinary kernel
path when its file descriptors close on process exit, and the rings
themselves are reference-counted, so they survive until the last
reference goes. KMES is unaffected and keeps writing regardless of
whether any consumer is attached — a system with no consumers behaves
identically to one with consumers, events simply being stamped,
buffered, and eventually overwritten. A restarted consumer re-attaches
and sees every surviving event, with the outage visible as a sequence
gap.

## 2.7.4 Buffer swap failure

If replacement rings cannot be allocated, the existing rings stay live
at their current size, the configuration change is not applied, no
generation changes, and consumers are unaffected. A
`KMES_BUFFER_SWAP_FAILED` event records the requested and retained
capacities (§2.6). KMES does not retry; the next configuration write
or a reboot triggers another attempt.

## 2.7.5 LCS unavailable

If no source ever registers, KMES runs indefinitely on compiled-in
defaults with the boot-time rings live and the configuration watch
never armed. This is a valid operating mode, not a failure — the only
consequence is that the parameters cannot be tuned.

## 2.7.6 Clock discontinuity

Timestamps come from `CLOCK_REALTIME`, so an NTP adjustment can move
them forward or backward and consumers sorting by timestamp will see
an apparent reordering. Sequence numbers are unaffected: they are
independent monotonic counters, never derived from the clock, and
remain the reliable ordering primitive within a CPU. KMES neither
detects nor compensates for discontinuities. Cross-CPU ordering near a
jump is best-effort — an inherent cost of wall-clock timestamps,
accepted for their readability and cross-boot comparability. Rate
limiting is immune, being driven by the monotonic clock.

System suspend and hibernate are special cases of the same thing. Ring
contents survive suspend to RAM, and are restored from the hibernate
image on resume; in both cases the clock jumps forward by the sleep
duration, so consumers see a wall-clock gap with no sequence gap.

## 2.7.7 CPU topology

The set of rings is fixed at initialisation from the kernel's
possible-CPU set, and topology changes are not handled dynamically.

A CPU that was possible but offline at initialisation already has a
ring: if it comes online later, its events go to that ring, and
consumers can attach to it throughout. A CPU whose logical index was
outside the possible-CPU set has no ring, and `kmes_attach` rejects
that index; KMES neither creates rings for such CPUs nor publishes
topology-change notifications. A CPU taken offline through `cpu_online` keeps its ring,
and a consumer draining it simply sleeps indefinitely, unable to
distinguish a quiet CPU from a departed one. Hot-add is the realistic
case — hypervisors adding vCPUs to a running guest — while hot-remove
is rare outside mainframes.

Anything that fixes this is additive: a topology-change notification,
whether a generation bump meaning "re-enumerate", a dedicated
descriptor, or a status field in the metadata page, fits the existing
attach-per-CPU design without changing the ring format, the event
format, or the emission API.

One structural caveat applies to attach discovery. The index bound is
the count of rings successfully created, while rings are indexed by
logical CPU id. On a system whose possible-CPU mask has holes, the two
differ: the "attach with incrementing indexes until `EINVAL`" loop
stops early, and rings at high logical indexes are unreachable.

Each logical CPU — each hardware thread under SMT — gets its own ring,
so ring memory scales with threads rather than cores: at the 4 MB
default a 64-core, 128-thread machine holds 512 MB of ring buffers.
This follows from events being emitted per logical CPU and `cpu_id`
naming the logical CPU.

## 2.7.8 Memory bounding

Ring memory in steady state is `num_cpus × BufferCapacity` for the
fixed CPU count fixed at initialisation, plus two metadata pages per CPU and one shmem page
per ring that has ever been attached. During a capacity swap old and
new rings coexist until every mapping of the old generation is
released.

Transient allocation during emission is bounded by the event size for
a single emit and freed as soon as the event is written. A batch is
different: every entry is staged in kernel memory simultaneously, so a
batch holds up to `count × MaxEventSize` — 256 entries at a 4 MB
maximum event size — until its write phase completes.

Each `kmes_attach` creates one file descriptor, bounded by
`RLIMIT_NOFILE` and by the SeSecurityPrivilege requirement. No
KMES-specific global memory cap exists; the capacity configuration and
standard Linux resource limits are the bounds.

## 2.7.9 Allocation and timing choices

The data region is a plain `vzalloc` allocation of 4 KB pages, and the
mmap handler inserts pages one at a time, so hugepage backing is not
available to it. The difference is substantial for TLB coverage: a 4 MB
ring needs 1024 standard pages against 2 hugepages, and because the
double mapping doubles the virtual range, 2048 standard pages against
4 hugepages. Allocation is not NUMA-aware: pages come from the
general allocator with no attempt to place a ring on the node local to
its CPU, so writes on a CPU whose ring landed on a remote node cross
the interconnect — roughly 100-150 ns against about 70 ns for a local
node. Timestamps use the full `ktime_get_real_ns()` rather
than `ktime_get_real_fast_ns()`, which avoids the timekeeper seqlock
and costs roughly 15-25 ns less per event at the price of being up to
one tick stale when a timer interrupt is updating the timekeeper
concurrently. Headers are built field by field
on every event with no precomputed per-CPU template, msgpack
validation is scalar, and each staged syscall event takes its own
`kvmalloc` rather than drawing on a per-CPU staging buffer.

None of these choices affects the ring format, the event header, or
the consumer protocol.

---

# Appendix 2.A KMES ABI Reference

_Peios / Advanced Peios / PKM / KMES_

> Every KMES syscall number, structure layout, ring-buffer offset and constant, generated from the uapi headers and measured by compilation.

Every name, value, offset and size in this appendix is generated from
`pkm/uapi/pkm/kmes.h` by `pkm/tools/gen-kmes-abi.py`, with struct
layouts measured by compiling a probe against the real header.
Regenerate it whenever the ABI changes; do not edit it by hand. The
names here are the ones a program actually compiles against.

What a compiler cannot measure -- the error vocabulary of each
syscall, the privilege each requires by name, what the configuration
keys do, and the implementation bounds that are not in the header --
is in the notes appendix, §2.B, which this generator does not touch.

## 2.A.1 Syscall numbers

Signatures are read from the `SYSCALL_DEFINE` sites in `pkm/kmes/`.

| Number | Constant | Signature |
|---|---|---|
| 1090 | `SYS_KMES_EMIT` | `kmes_emit(const char __user *event_type, u16 event_type_len, const void __user *payload, u32 payload_len)` |
| 1091 | `SYS_KMES_ATTACH` | `kmes_attach(unsigned int cpu_id, u64 __user *capacity)` |
| 1092 | `SYS_KMES_EMIT_BATCH` | `kmes_emit_batch(const struct kmes_emit_entry __user *entries, u32 count, u32 __user *emitted_out)` |

## 2.A.2 Structure layouts

Offsets and sizes are measured, not declared.

### 2.A.2.1 `struct kmes_emit_entry`

Total size 32 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 8 | `__u64` | `event_type` |
| 8 | 2 | `__u16` | `event_type_len` |
| 10 | 6 | `__u8[6]` | `_pad0` |
| 16 | 8 | `__u64` | `payload` |
| 24 | 4 | `__u32` | `payload_len` |
| 28 | 4 | `__u8[4]` | `_pad1` |

## 2.A.3 Constants

Grouped as the header groups them.

*Event origin class — kmes_event_header.origin_class.*

| Constant | Value |
|---|---|
| `KMES_ORIGIN_USERSPACE` | `0` |
| `KMES_ORIGIN_KMES` | `1` |
| `KMES_ORIGIN_KACS` | `2` |
| `KMES_ORIGIN_LCS` | `3` |

*Ring-slot discovery.*

Ring slots are indexed by logical CPU id and the array is sized by the
kernel's nr_cpu_ids, so a slot inside the array holds no ring when that
CPU is not possible. Counting up from 0 until SYS_KMES_ATTACH returns
-EINVAL therefore stops at the first hole and misses every ring above
it, leaving those CPUs' events permanently unreachable.

Call SYS_KMES_ATTACH with cpu_id KMES_ATTACH_QUERY_SLOTS to learn the
slot count instead. It writes the count through the capacity argument,
returns 0, and opens no descriptor. Enumerate 0 .. count-1 and treat
-EINVAL as "this slot holds no ring", not as the end of the array.

The sentinel is outside the index space for good: nr_cpu_ids is bounded
by CONFIG_NR_CPUS, which cannot reach 2^32-1.

| Constant | Value |
|---|---|
| `KMES_ATTACH_QUERY_SLOTS` | `0xFFFFFFFF` |

*Largest entry count a single SYS_KMES_EMIT_BATCH call accepts.*

| Constant | Value |
|---|---|
| `KMES_BATCH_MAX_ENTRIES` | `256` |

*Runtime configuration registry location and keys.*

Type values match the LCS REG_\* constants: REG_DWORD is 4 and REG_QWORD
is 11. They are repeated here so &lt;pkm/kmes.h&gt; remains standalone.

| Constant | Value |
|---|---|
| `KMES_CONFIG_ROOT_HIVE` | `"Machine"` |
| `KMES_CONFIG_ROOT_SYSTEM_KEY` | `"System"` |
| `KMES_CONFIG_ROOT_KMES_KEY` | `"KMES"` |
| `KMES_CONFIG_KEY_BUFFER_CAPACITY` | `"BufferCapacity"` |
| `KMES_CONFIG_KEY_MAX_EVENT_SIZE` | `"MaxEventSize"` |
| `KMES_CONFIG_KEY_MAX_NESTING_DEPTH` | `"MaxNestingDepth"` |
| `KMES_CONFIG_KEY_MAX_EMIT_RATE_PER_PROCESS` | `"MaxEmitRatePerProcess"` |
| `KMES_CONFIG_TYPE_REG_DWORD` | `4` |
| `KMES_CONFIG_TYPE_REG_QWORD` | `11` |
| `KMES_CONFIG_BUFFER_CAPACITY_TYPE` | `11` |
| `KMES_CONFIG_BUFFER_CAPACITY_DEFAULT` | `4194304` |
| `KMES_CONFIG_BUFFER_CAPACITY_MIN` | `65536` |
| `KMES_CONFIG_BUFFER_CAPACITY_MAX` | `268435456` |
| `KMES_CONFIG_MAX_EVENT_SIZE_TYPE` | `4` |
| `KMES_CONFIG_MAX_EVENT_SIZE_DEFAULT` | `65536` |
| `KMES_CONFIG_MAX_EVENT_SIZE_MIN` | `1024` |
| `KMES_CONFIG_MAX_EVENT_SIZE_MAX` | `4194304` |
| `KMES_CONFIG_MAX_NESTING_DEPTH_TYPE` | `4` |
| `KMES_CONFIG_MAX_NESTING_DEPTH_DEFAULT` | `32` |
| `KMES_CONFIG_MAX_NESTING_DEPTH_MIN` | `4` |
| `KMES_CONFIG_MAX_NESTING_DEPTH_MAX` | `256` |
| `KMES_CONFIG_MAX_EMIT_RATE_PER_PROCESS_TYPE` | `4` |
| `KMES_CONFIG_MAX_EMIT_RATE_PER_PROCESS_DEFAULT` | `10000` |
| `KMES_CONFIG_MAX_EMIT_RATE_PER_PROCESS_MIN` | `100` |
| `KMES_CONFIG_MAX_EMIT_RATE_PER_PROCESS_MAX` | `1000000` |

*Privilege requirements.*

Values mirror the corresponding KACS privilege bits while keeping this
header standalone.

| Constant | Value |
|---|---|
| `KMES_EMIT_REQUIRED_PRIVILEGE` | `0x0000000000200000` (1ULL << 21) |
| `KMES_ATTACH_REQUIRED_PRIVILEGE` | `0x0000000000000100` (1ULL << 8) |

*On-wire event header.*

Every event in a ring begins with a fixed 77-byte header, followed by
event_type_len bytes of type string and then the msgpack payload. Events
abut at event_size stride, so a header is not generally aligned; it
crosses the ABI as raw bytes, not a C struct. Its fields, in order:

```text
__u32  event_size            total event byte length (header + type + payload)
__u32  header_size           byte offset from the event start to the payload
__u64  timestamp_ns
__u64  sequence
__u16  cpu_id
__u8   origin_class          one of KMES_ORIGIN_* above
__u8   effective_token_guid[16]
__u8   true_token_guid[16]
__u8   process_guid[16]
__u16  event_type_len        length of the type string following the header
```

The three GUIDs are 16-byte Microsoft GUID binary values
(Data1/Data2/Data3 little-endian, Data4 raw), captured from KACS at
emission time; the null GUID (16 zero bytes) means identity was
unavailable (KACS not initialised, or no process context). KMES copies
them opaquely.

Read each field at its KMES_EVENT_\*_OFFSET below. header_size locates
the payload: it is KMES_EVENT_HEADER_BASE_SIZE + event_type_len. A
future revision may grow the header, so consumers must use header_size,
not the end of the type string, to find the payload.

| Constant | Value |
|---|---|
| `KMES_EVENT_SIZE_OFFSET` | `0` |
| `KMES_EVENT_HEADER_SIZE_OFFSET` | `4` |
| `KMES_EVENT_TIMESTAMP_NS_OFFSET` | `8` |
| `KMES_EVENT_SEQUENCE_OFFSET` | `16` |
| `KMES_EVENT_CPU_ID_OFFSET` | `24` |
| `KMES_EVENT_ORIGIN_CLASS_OFFSET` | `26` |
| `KMES_EVENT_EFFECTIVE_TOKEN_GUID_OFFSET` | `27` |
| `KMES_EVENT_TRUE_TOKEN_GUID_OFFSET` | `43` |
| `KMES_EVENT_PROCESS_GUID_OFFSET` | `59` |
| `KMES_EVENT_TYPE_LEN_OFFSET` | `75` |

*Byte width of each identity GUID in the event header.*

| Constant | Value |
|---|---|
| `KMES_EVENT_GUID_SIZE` | `16` |

*Byte size of the fixed event header — the offset at which the type string begins.*

| Constant | Value |
|---|---|
| `KMES_EVENT_HEADER_BASE_SIZE` | `77` |

*Ring-buffer metadata layout.*

An attached ring is mmap'd as:

```text
page 0          producer metadata (read-only to the consumer)
page 1          consumer metadata (read-write)
ring data       the event bytes, mapped twice back-to-back so an event
                that wraps the buffer end is still contiguous.
```

The producer metadata page begins with KMES_RING_MAGIC.

| Constant | Value |
|---|---|
| `KMES_RING_MAGIC` | `"KMESRING"` |
| `KMES_RING_VERSION` | `1` |
| `KMES_METADATA_PAGE_SIZE` | `4096` |
| `KMES_METADATA_TOTAL_SIZE` | `8192` |
| `KMES_MAPPING_PRODUCER_OFFSET` | `0` |
| `KMES_MAPPING_CONSUMER_OFFSET` | `4096` |
| `KMES_MAPPING_DATA_OFFSET` | `8192` |

*Field offsets within the producer metadata page.*

| Constant | Value |
|---|---|
| `KMES_PRODUCER_MAGIC_OFFSET` | `0` |
| `KMES_PRODUCER_VERSION_OFFSET` | `8` |
| `KMES_PRODUCER_CPU_ID_OFFSET` | `12` |
| `KMES_PRODUCER_CAPACITY_OFFSET` | `16` |
| `KMES_PRODUCER_DATA_OFFSET_OFFSET` | `24` |
| `KMES_PRODUCER_GENERATION_OFFSET` | `32` |
| `KMES_PRODUCER_WRITE_POS_OFFSET` | `64` |
| `KMES_PRODUCER_TAIL_POS_OFFSET` | `72` |
| `KMES_PRODUCER_FUTEX_COUNTER_OFFSET` | `128` |

*Field offset within the consumer metadata page.*

| Constant | Value |
|---|---|
| `KMES_CONSUMER_NEED_WAKE_OFFSET` | `0` |

## 2.A.4 Tracepoint diagnostic codes

From `uapi/pkm/trace.h`. These are a diagnostic contract for
ftrace, perf and eBPF consumers, letting a tool decode a `kmes:`
event's `reason`, `op` or `state` field without recompiling
against a specific kernel. No KMES syscall accepts or returns
them, and values are append-only.

*kmes_drop reason — why the KMES ring machinery lost an event.*

RING_FULL is a normal overwrite; TAIL_RESYNC is the silent corruption-
recovery path that discards ALL pending events; VALIDATE is a kernel-
emit size/type reject at the ring boundary; BATCH_STRUCT_INVALID is a
per-entry structural reject in the kernel batch path. Emitted by
kmes:kmes_drop. No event payload bytes.

| Constant | Value | Notes |
|---|---|---|
| `KMES_DROP_RING_FULL` | `0` | capacity overwrite; oldest event dropped |
| `KMES_DROP_TAIL_RESYNC` | `1` | corrupt ring header; pending events silently discarded |
| `KMES_DROP_VALIDATE` | `2` | single kernel-emit size/type reject |
| `KMES_DROP_BATCH_STRUCT_INVALID` | `3` | kernel-batch entry structurally invalid |

*kmes_swap reason — a bounded ring capacity swap lifecycle marker.*

BEGIN and COMPLETE/FAILED are whole-topology (cpu field is U16_MAX);
MIGRATE_SKIP is per-CPU and carries the skipped byte count in `ret`.
Emitted by kmes:kmes_swap.

| Constant | Value | Notes |
|---|---|---|
| `KMES_SWAP_BEGIN` | `0` | capacity change accepted, rings allocating |
| `KMES_SWAP_COMPLETE` | `1` | swap committed across all CPUs |
| `KMES_SWAP_MIGRATE_SKIP` | `2` | shrink: old event too large, skipped (ret=bytes) |
| `KMES_SWAP_FAILED` | `3` | swap aborted; ret is the errno |

*kmes_rate reason — the per-process token-bucket backpressure signal.*

THROTTLE is an -EAGAIN emit rejection; RECONFIGURE marks an admin rate
change clamping all buckets. Emitted by kmes:kmes_rate.

| Constant | Value | Notes |
|---|---|---|
| `KMES_RATE_THROTTLE` | `0` | emit denied -EAGAIN; tokens &lt; requested |
| `KMES_RATE_RECONFIGURE` | `1` | max emit rate reconfigured for all buckets |

*kmes_wake reason — consumer wakeup machinery.*

NOTE arms a pending wake; FUTEX is the actual futex wake of blocked
consumers. Emitted by kmes:kmes_wake.

| Constant | Value | Notes |
|---|---|---|
| `KMES_WAKE_NOTE` | `0` | wake armed; futex counter incremented |
| `KMES_WAKE_FUTEX` | `1` | blocked consumers woken |

*kmes_ring_lifecycle reason — a generation-stable ring object transition.*

`ret` is the outcome. Emitted by kmes:kmes_ring_lifecycle.

| Constant | Value | Notes |
|---|---|---|
| `KMES_RING_ALLOC` | `0` | ring backing allocated (or -ENOMEM) |
| `KMES_RING_FREE` | `1` | ring backing released |
| `KMES_RING_PRODUCER_PAGE` | `2` | producer shmem/meta page attached |
| `KMES_RING_CONSUMER_FD` | `3` | consumer anon-inode fd created |

*kmes_ingress_reject reason — why an emit request was rejected before the ring.*

OVER_MAX/OVER_CAP_HALF/SIZE_OVERFLOW are declared-size rejects;
EMIT_OVERSIZE is a staged event too large at ring-write time;
BATCH_PARTIAL marks a batch that validated fewer entries than requested.
Emitted by kmes:kmes_ingress_reject.

| Constant | Value | Notes |
|---|---|---|
| `KMES_INGRESS_OVER_MAX` | `0` | event_size exceeds configured max_event_size |
| `KMES_INGRESS_OVER_CAP_HALF` | `1` | event_size exceeds ring_capacity/2 |
| `KMES_INGRESS_SIZE_OVERFLOW` | `2` | declared header/event size overflow |
| `KMES_INGRESS_EMIT_OVERSIZE` | `3` | staged event exceeds live capacity/2 at emit |
| `KMES_INGRESS_BATCH_PARTIAL` | `4` | batch staged fewer entries than requested |

*kmes_validate reason — the C-boundary result of the Rust staged-event validator.*

The Rust side collapses its structural checks into one nonzero return;
only visible type/payload lengths and `ret` are recorded here. Emitted
by kmes:kmes_validate.

| Constant | Value | Notes |
|---|---|---|
| `KMES_VAL_EINVAL` | `0` | Rust msgpack/structural validation rejected |

---

# Appendix 2.B KMES ABI Notes

_Peios / Advanced Peios / PKM / KMES_

> What the KMES ABI tables cannot say for themselves — what each syscall parameter means, the privileges each requires, the error vocabulary, the implementation bounds outside the header, and the build configuration.

§2.A is generated from `pkm/uapi/pkm/kmes.h` and holds only what a
compiler can measure. This appendix holds the rest.

The split is structural rather than editorial. `gen-kmes-abi.py`
overwrites §2.A wholesale on every run, so anything written there is
lost the next time the ABI changes.

Layouts that form part of the consumer contract — the event header,
the producer and consumer metadata pages, and the mapped region — are
specified normatively in the PSPK event stream specification. §2.A
gives their offsets as the header defines them; the specification
governs.

## 2.B.1 Syscall parameters

| Syscall | Parameter | Type | Meaning |
|---|---|---|---|
| `kmes_emit` | `event_type` | `const char *` | Event type string. |
| | `event_type_len` | `u16` | Its length in bytes. |
| | `payload` | `const void *` | MessagePack payload. |
| | `payload_len` | `u32` | Its length in bytes. |
| `kmes_emit_batch` | `entries` | `struct kmes_emit_entry *` | Array of event descriptors. |
| | `count` | `u32` | Number of entries, 1 to `KMES_BATCH_MAX_ENTRIES`. |
| | `emitted_out` | `u32 *` | Receives the number of events emitted. |
| `kmes_attach` | `cpu_id` | `unsigned int` | Ring slot index, or `KMES_ATTACH_QUERY_SLOTS` to query the slot count. |
| | `capacity` | `u64 *` | Receives the ring buffer capacity in bytes. |

`kmes_emit` and `kmes_emit_batch` return 0 on success; `kmes_attach`
returns a file descriptor. All three return -1 and set errno on
failure.

The PKM syscall range is 1090–1099; KMES uses the first three.

## 2.B.2 Privilege requirements

`kmes.h` gives these as bit masks so it can stand alone. The names
belong to the KACS privilege catalogue, and the bit index is what the
two agree on.

| Operation | Required privilege | Bit |
|---|---|---|
| `kmes_emit`, `kmes_emit_batch` | SeAuditPrivilege | 21 |
| Rate-limit exemption on both | SeTcbPrivilege | 7 |
| `kmes_attach` | SeSecurityPrivilege | 8 |

Holding a privilege is not enough: it must be *enabled*, and KMES marks
it used before proceeding. A failure to record the used state is itself
an `EPERM`, because an unrecorded privilege use is an audit gap.

SeTcbPrivilege is checked but not required — an emitter that holds it
enabled is exempt from the per-process rate limit, and one that does not
is throttled.

## 2.B.3 Implementation bounds

These are properties of the implementation rather than of the ABI, so
they are not in `kmes.h` and a program must not compile against them.
They bound what the interface will accept.

| Quantity | Value | Where |
|---|---|---|
| Maximum event size, structural | 50% of ring capacity | `kmes/kmes.c` |
| MessagePack validator nesting stack | 256 | `kmes/kmes_validate.rs` |
| Self-configuration payload buffer | 768 bytes | `kmes/kmes.c` |
| Self-configuration audit intents per read | 4 | `kmes/kmes.h` |
| Self-configuration parameter name | 64 bytes | `kmes/kmes.h` |

The structural 50% bound is independent of `MaxEventSize` and applies to
kernel emitters too. An event may satisfy the configured maximum and
still be refused because the ring is small.

The validator's 256-frame stack is why `MaxNestingDepth` has a maximum of
256. The two bounds are enforced independently: the configuration range
check refuses a larger value at apply time, and the validator refuses
every event outright if it is somehow handed one, rather than silently
accepting nesting it cannot track.

## 2.B.4 Configuration keys

Registry path `Machine\System\KMES\`. The type codes, defaults and
ranges are in §2.A; what each key does is here.

| Key | Effect |
|---|---|
| `BufferCapacity` | Per-CPU ring size in bytes. Must be a power of two. Changing it swaps every ring; see §2.6. |
| `MaxEventSize` | Largest event a syscall emitter may produce. |
| `MaxNestingDepth` | Deepest MessagePack container nesting the validator will accept. |
| `MaxEmitRatePerProcess` | Token-bucket rate, events per second, per process. |

`MaxEventSize`, `MaxNestingDepth` and `MaxEmitRatePerProcess` apply only
to syscall emitters. A kernel emitter is not rate-limited and its payload
is not parsed as MessagePack, but it is still checked structurally —
non-empty type string within `PKM_KMES_MAX_KERNEL_TYPE_LEN`, a payload
pointer if the length is non-zero, no size arithmetic overflow, and the
50% bound. A kernel event that fails is dropped with
`KMES_DROP_VALIDATE`, not emitted.

A key that is missing, of the wrong type, or out of range does not fail
the read: the previous value is retained and the disagreement is
recorded as an audit intent. At most four such intents are carried out
of one read, so a configuration with five bad keys reports four.

## 2.B.5 Error codes

### 2.B.5.1 `kmes_emit`

| Errno | Condition |
|---|---|
| `EPERM` | SeAuditPrivilege not held or not enabled, or recording its used state failed. |
| `EAGAIN` | Per-process rate limit exceeded. |
| `EINVAL` | Zero event type length, event type not valid UTF-8, declared size arithmetic overflowed, payload not valid msgpack, or nesting depth over `MaxNestingDepth`. |
| `EFAULT` | Event type or payload pointer inaccessible. |
| `ENOSPC` | Event exceeds `MaxEventSize` or 50% of ring capacity. |
| `ENOMEM` | Staging buffer allocation failed, or KMES not initialised. |

### 2.B.5.2 `kmes_emit_batch`

| Errno | Condition |
|---|---|
| `EPERM` | As `kmes_emit`. |
| `EAGAIN` | Fewer than `count` tokens available. |
| `EINVAL` | `count` is 0 or over `KMES_BATCH_MAX_ENTRIES`, or the failing entry hit one of `kmes_emit`'s `EINVAL` conditions. |
| `EFAULT` | `emitted_out`, the entry array, or the failing entry's type or payload pointer inaccessible. |
| `ENOSPC` | The failing entry exceeds `MaxEventSize` or 50% of ring capacity. |
| `ENOMEM` | Kernel allocation failed, or KMES not initialised. |

### 2.B.5.3 `kmes_attach`

| Errno | Condition |
|---|---|
| `EPERM` | SeSecurityPrivilege not held or not enabled, or recording its used state failed. |
| `EINVAL` | `cpu_id` at or beyond the ring array size, or its slot holds no live ring. |
| `EFAULT` | `capacity` pointer inaccessible. |
| `ENOMEM` | Kernel allocation failed, or KMES not initialised. |

A `KMES_ATTACH_QUERY_SLOTS` call takes the same `EPERM`, `EFAULT` and
`ENOMEM` conditions and cannot return `EINVAL`.

The ring array is sized by `nr_cpu_ids`, not by the number of rings
allocated. On a machine with a sparse possible-CPU mask the two differ,
and a slot inside the array with no live ring returns `EINVAL` exactly
as an index beyond the array does. A consumer therefore enumerates
against the slot count from `KMES_ATTACH_QUERY_SLOTS` and skips the
`EINVAL` slots rather than stopping at the first one; see §2.4.

## 2.B.6 Build configuration

KMES is built by `CONFIG_SECURITY_PKM`, a boolean option, so it is
linked into `vmlinux` rather than loaded. `CONFIG_RUST=y` is required:
the MessagePack validator is Rust. The whole subsystem is staged into
the kernel tree as `security/pkm/kmes` by `pkm/kernel/stage-sources.sh`,
which also stages `<trace/events/kmes.h>` so the tracepoints resolve.

The three syscall numbers are added to the syscall table by
`kernel/patches/arch/syscall-table-pkm.patch`, which patches both
`arch/x86/entry/syscalls/syscall_64.tbl` and the copy of it that ships
under `tools/perf/`. They are registered `common`, so they are reachable
from the x32 ABI as well as from x86-64.

`CONFIG_SECURITY_PKM_KUNIT` compiles in the in-kernel test harness.

---

# 3.1 Overview

_Peios / Advanced Peios / PKM / KACS_

> KACS is the security core of Peios — an LSM inside PKM providing identity-based access control — and how it relates to MS-DTYP.

The Kernel Access Control System is the security core of Peios: an LSM
within PKM providing identity-based access control in the Linux
kernel. It is the sole identity-based authorization mechanism for
managed objects. Every identity-based decision — a file open, a
registry read, an IPC connection, a signal, a token operation — passes
through one evaluation function, AccessCheck.

This chapter covers tokens (§3.2), the Process Security Block (§3.3),
privileges (§3.4), impersonation (§3.5), binary signature verification
(§3.6), Process Integrity Protection (§3.7), the AccessCheck algorithm
in full (§3.8), file enforcement (§3.9), and how Peios identity is
projected onto the Linux credential model (§3.10).

Two bodies of material that KACS owns conceptually live elsewhere in
the documentation. The binary structures — SIDs, security descriptors,
ACLs, ACEs, access masks, conditional ACE bytecode — are specified in
PCDS, because userspace tooling constructs and interprets them and has
to agree with the kernel byte for byte. The signing format a third
party would use to sign a binary with a PIP level is specified in
PSPK; this chapter describes only the verification side. What remains
here is the kernel's own behaviour: how it holds identity, and how it
decides.

## 3.1.1 Terminology

A **token** is a per-thread identity object held in the kernel's
credential structure, carrying a user SID, group SIDs, a privilege
bitmask, an integrity level, an impersonation level, and metadata.
Identity fields — SIDs, type, integrity level — are immutable; policy
fields — enabled privileges, enabled groups, default owner, group and
DACL — are atomically adjustable. Every thread has a token, and there
is no such thing as a null token.

A **primary token** defines a process's baseline identity and is
inherited on fork, reached through `task->real_cred`. An
**impersonation token** temporarily overrides it for access decisions
on one thread only, reached through `task->cred`.

A **LogonSession** is a kernel object representing one authentication
event: a session ID, a logon type, a user SID, an authentication
package, a logon time, and a logon SID. Tokens reference their session
by ID.

A **privilege** is a system-wide right carried on a token. Some
influence AccessCheck; others gate a standalone operation.

An **impersonation level** controls how far an identity can travel:
Anonymous, Identification, Impersonation, or Delegation.

An **integrity level** is a vertical trust classification on tokens
and objects. Numerically it is the mandatory label SID's single
sub-authority compared as an unsigned integer, so any `S-1-16-<n>`
with exactly one sub-authority is valid. In practice five standard
levels form a strict total order — Untrusted (0), Low (4096), Medium
(8192), High (12288), System (16384) — and non-standard values such as
`S-1-16-8448` appear only in SDs authored for Windows interop.
**Mandatory Integrity Control** is the constraint evaluated before the
DACL, blocking write access, and optionally read and execute, when the
caller's level is below the object's label.

**Process Integrity Protection** is a two-dimensional trust model —
type against trust level — protecting processes and objects from
insufficiently trusted callers. Unlike MIC, it revokes rights that a
privilege would otherwise have granted.

The **Process Security Block** is a per-process structure carrying PIP
identity, process mitigations, and process restrictions. It is never
affected by impersonation, which is the point of keeping it separate
from the token.

**FACS**, the File Access Control Shim, is the part of KACS that
replaces Linux DAC with security-descriptor evaluation on files. It
enforces the handle model: AccessCheck runs at open time and the
granted mask is cached on the file description, with later operations
checked against the cached mask.

An **object type** is the category of a protected resource. Each
defines a GenericMapping table translating generic rights into
object-specific ones.

The **TCB** — the components whose correct behaviour is necessary for
system security — is the Linux kernel, PKM, and the core trusted
userspace daemons: peinit, authd, and loregd.

## 3.1.2 Relationship to MS-DTYP

KACS is not a port of another system's security model. Tokens,
security descriptors, AccessCheck, structured SIDs, and per-thread
impersonation were chosen because they solve what Peios needs solved:
coherent identity, rich per-object access control, scoped delegation,
and integrated audit.

Those same primitives are the ones Active Directory uses, and Peios is
built to join AD domains as a first-class member — exchanging security
data with domain controllers, authenticating through Kerberos, and
enforcing policy distributed by Group Policy. That imposes binary
format compatibility, which PCDS specifies: an SD written by a Windows
domain controller and replicated through Samba is evaluated by KACS
without translation.

Format compatibility does not imply evaluator compatibility in every
corner. Given the same token, descriptor, and desired mask, KACS
generally reaches the same decision MS-DTYP describes, which is what
makes policy authored in an AD environment behave predictably here.
Where it deliberately does not, §3.B records every departure and why.

---

# 3.2.1 The Token Model

_Peios / Advanced Peios / PKM / KACS / Tokens_

> A token is a thread's identity and security policy — how it relates to Linux credentials, primary against effective, and the evaluation context.

A token is a kernel object representing a thread's identity and
security policy: the user's SID, group memberships, privileges,
integrity level, impersonation state, claims, confinement settings,
and metadata. Every KACS-mediated access control decision evaluates
the thread's effective token.

Every live userspace thread has one. KACS-mediated authorization never
evaluates a null token — credentials that are blank, uncommitted,
kernel-only, asynchronous, or otherwise outside a meaningful userspace
evaluation context may carry no token at all, and an LSM hook that
reaches KACS with such a credential fails closed rather than evaluate
a meaningless identity.

## 3.2.1.1 Relationship to Linux credentials

Tokens are independently allocated, reference-counted kernel objects.
A `struct cred` holds a *pointer* to the token in its LSM security
blob, not the token data itself.

That indirection is architecturally load-bearing. Linux credentials
are immutable once committed: after `commit_creds()` a `struct cred`
cannot be modified. Had token data been embedded in the credential,
every token mutation — toggling a privilege, adjusting a group — would
have required allocating a whole new credential. Keeping the token
behind a pointer lets token-internal mutations use the token's own
synchronisation and leave the credential alone.

Several thread credentials within a process may reference one token
object through `real_cred`, and a mutation to a shared token is
visible to every thread sharing it. At fork the child receives an
independent deep copy, so mutations after fork are invisible across
the process boundary.

## 3.2.1.2 Primary and effective tokens

The `real_cred`/`cred` split on `task_struct` carries two roles.

`real_cred` is the task's **objective** identity, used when other
tasks evaluate access *to* this task. Its LSM blob points at the
**primary token** — the process's baseline identity, inherited from
the parent at fork.

`cred` is the task's **subjective** identity, used when this task
evaluates access to other objects. Its blob points at the **effective
token**: normally the primary token, or an impersonation token
installed by a server thread acting for a client.

With no impersonation in play, `real_cred` and `cred` are the same
credential and resolve to the same token. Impersonation swaps `cred`
to a new credential pointing at a different token; reverting restores
`cred` to `real_cred`.

## 3.2.1.3 Evaluation context

Token evaluation — AccessCheck, privilege checks — happens only where
a meaningful subject authority exists. In task context that authority
is the effective token reached through `current_cred()`.

Linux credential substitution is authoritative for deferred work. When
a kernel path runs under credentials installed by `override_creds()`
or another subjective-credential mechanism, the token pointer in that
credential's blob travels with it and is evaluated normally. KACS does
not strip authority merely because the current task carries a
kernel-thread, workqueue, or io_uring-worker flag — the credential is
what counts, not the worker flag.

Authorization already cached on an object handle, such as the granted
mask on a file description, continues to use that cached authority for
post-open operations. User-originated asynchronous work that has
neither captured credentials nor cached handle authority reaches KACS
without a token-bearing credential and fails closed.

Kernel-originated infrastructure work running under the boot SYSTEM
credential evaluates as SYSTEM. There is no separate worker-flag-based
kernel authority identity.

---

# 3.2.2 Token Structure

_Peios / Advanced Peios / PKM / KACS / Tokens_

> Every token field by mutability class — fixed identity, adjustable privileges, one-way elevation — and what each carries.

Token fields fall into three mutability classes. **Fixed** fields are
set at creation and never change — every security-critical identity
field is fixed. **Adjustable** fields can be modified at runtime
through the adjustment operations (§3.2.5). **One-way** fields can be
set or tightened but never cleared or loosened.

## 3.2.2.1 Identity core (fixed)

| Field | Type | Description |
|---|---|---|
| `user_sid` | SID | The token's primary identity. |
| `user_deny_only` | bool | When true, the user SID matches only deny ACEs, never allow ACEs. Set at creation by CreateToken or FilterToken. True whenever `write_restricted` is true. |
| `groups` | SID_AND_ATTRIBUTES[] | Group memberships. The set of SIDs is fixed at creation; the per-group attribute flags are adjustable. |
| `restricted_sids` | SID_AND_ATTRIBUTES[]? | Secondary SID list for restricted tokens, null on unrestricted ones. Set at creation by CreateToken or FilterToken. AccessCheck treats the list as presence-based: a restricting SID participates whenever it is present, and neither `SE_GROUP_ENABLED` nor `SE_GROUP_USE_FOR_DENY_ONLY` affects restricted-pass matching. |
| `write_restricted` | bool | When true, the restricted SID check applies only to write access. Set at creation by CreateToken or FilterToken. |

The **logon SID** — `S-1-5-5-X-Y`, tying the token to its
LogonSession — is not stored as a token field at all. It is derived
from the session on every read, and materialised once in `groups`
carrying `SE_GROUP_LOGON_ID`, which is where AccessCheck finds it.

The group SID set is fixed at creation — adjustment never adds or
removes a SID. Individual groups can be enabled or disabled by
modifying `SE_GROUP_ENABLED`, within two limits: a mandatory group
(`SE_GROUP_MANDATORY`) cannot be disabled, and a deny-only group
(`SE_GROUP_USE_FOR_DENY_ONLY`) cannot be re-enabled.

A token holds at most 1024 group entries including the kernel-injected
logon SID, so CreateToken accepts at most 1023 caller-supplied groups.

## 3.2.2.2 Token type (fixed)

| Field | Type | Description |
|---|---|---|
| `token_type` | enum | Primary or Impersonation. |
| `impersonation_level` | enum | Anonymous, Identification, Impersonation, or Delegation. Primary tokens always carry Anonymous. |

## 3.2.2.3 Integrity (fixed)

| Field | Type | Description |
|---|---|---|
| `integrity_level` | uint | Numeric integrity level. Standard values are 0 (Untrusted), 4096 (Low), 8192 (Medium), 12288 (High), 16384 (System), but any unsigned integer is valid and is compared numerically against object labels. |
| `mandatory_policy` | flags | `NO_WRITE_UP` (0x0001) and `NEW_PROCESS_MIN` (0x0002). Per-token MIC enforcement policy, set at creation. |

`mandatory_policy` is immutable: a process cannot change its own MIC
constraints at runtime, neither loosening nor tightening them. This is
a deliberate departure from MS-DTYP, where the mandatory policy is
runtime-modifiable — which reduces MIC to a constraint that stops only
processes not actively trying to bypass it. Immutability is what makes
MIC a real boundary here, and it is also what allows the impersonation
integrity ceiling (§3.5) to be enforced unconditionally.

## 3.2.2.4 Privileges (adjustable)

Each privilege on a token has four independent states.

**Present** — the privilege exists on the token. A present privilege
can be removed permanently, but no privilege can be added after
creation. **Enabled** — the privilege is currently active; only
present privileges can be enabled or disabled. **Enabled by default**
— the creation-time enabled state, which adjustment can restore.
**Used** — the privilege has been exercised during this token's
lifetime; monotonic, and never cleared once set.

The lifecycle runs: present and disabled, to enabled, to used, then
optionally disabled, then optionally removed permanently. Removal
clears the privilege from the present, enabled, and enabled-by-default
states together.

The four states are encoded as four 64-bit bitmasks, one bit per
defined privilege, which makes a privilege check constant-time and a
multi-privilege operation atomic.

## 3.2.2.5 Elevation (one-way)

| Field | Type | Description |
|---|---|---|
| `elevation_type` | enum | Default (non-elevated), Full (elevated), or Limited (filtered). |

A token is created as Default. Only `KACS_IOC_LINK_TOKENS` sets Full
or Limited, when a linked pair is established, and once set neither
reverts to Default on that token object. The role is sticky: relinking
can replace the partner but never converts Full to Limited or the
reverse. DuplicateToken and FilterToken produce new token objects
whose `elevation_type` starts again at Default, because a new token is
not part of any linked pair.

Linked pairs are associated at the LogonSession level rather than
stored on individual tokens; §3.2.6 describes the pairing mechanism.

## 3.2.2.6 Default object security (adjustable)

| Field | Description |
|---|---|
| `owner_sid_index` | Index into `[user_sid, groups...]` selecting the default owner SID for new objects: 0 is the user SID, 1..N are `groups[0..N-1]`. The referenced SID is the user SID or a group carrying `SE_GROUP_OWNER`. |
| `primary_group_index` | Index into `[user_sid, groups...]` selecting the default primary group. The referenced SID is the user SID or any group SID on the token. |
| `default_dacl` | The DACL applied to objects this token creates when no explicit descriptor is supplied. |

Storing indices rather than SID copies keeps the owner and primary
group consistent with the group array they name.

## 3.2.2.7 Metadata (fixed)

| Field | Type | Description |
|---|---|---|
| `token_id` | LUID | Unique identifier for this token instance. |
| `token_guid` | UUID | 128-bit identifier for this token instance, generated by the kernel at creation and immutable. Used by KMES and other kernel-internal consumers for identity stamping and event correlation. |
| `auth_id` | LUID | The LogonSession LUID, linking the token to the authentication event that produced it. |
| `source` | TOKEN_SOURCE | Who minted the token: an 8-character name plus a LUID. |
| `created_at` | timestamp | When the original token was minted by CreateToken. Copied unchanged by DuplicateToken, FilterToken, and `NEW_PROCESS_MIN`, so it tracks original minting rather than duplication. |
| `expiration` | timestamp | When the token becomes invalid; zero means no expiry. Not enforced by AccessCheck. |
| `origin` | LUID | The originating LogonSession for derived tokens, such as S4U or network logon. |

## 3.2.2.8 Mutation tracking (adjustable)

| Field | Description |
|---|---|
| `modified_id` | Counter incremented on any token adjustment. |

Marking a privilege used is audit and accounting state rather than an
access-decision input, so it stays monotonic but does not bump
`modified_id`. Setting the elevation type is the one other mutation
that leaves the counter alone.

The counter is intended as a cache invalidation key — a `modified_id`
that has changed since a cached decision was taken means the decision
is stale. Nothing currently reads it for that purpose: it is
maintained on every adjustment and reported through the statistics
query class, and no cache anywhere invalidates on it.

## 3.2.2.9 Interactivity scope (adjustable)

| Field | Description |
|---|---|
| `interactivity_scope` | 0 for services, which have no interactive environment; 1 and above for interactive and remote user environments. Changing it requires `SeTcbPrivilege`. |

## 3.2.2.10 Claims and security attributes (fixed)

| Field | Type | Description |
|---|---|---|
| `user_claims` | CLAIM_ATTRIBUTES[] | Name-value pairs from the user's directory object, fed into conditional ACE evaluation. |
| `device_claims` | CLAIM_ATTRIBUTES[] | Name-value pairs from the machine's directory object. |

## 3.2.2.11 LCS registry credentials (fixed)

| Field | Type | Description |
|---|---|---|
| `lcs_scope_guids` | GUID[] | Ordered private registry scope GUIDs used by LCS private hive routing. LCS checks the list in order before falling back to global hives. |
| `lcs_private_layers` | string[] | Registry layer names visible to this token even when disabled globally, using LCS layer-name syntax and matching rules. |

These are KACS-owned credential material belonging to LCS. They are
fixed at creation and copied by duplication and filtering, and
attaching them is authorized by the same trusted-minting gate as the
rest of CreateToken — only a caller holding `SeCreateTokenPrivilege`
can create a token carrying them.

## 3.2.2.12 Device identity (fixed)

| Field | Type | Description |
|---|---|---|
| `device_groups` | SID_AND_ATTRIBUTES[]? | The machine's group memberships, for compound identity. |
| `restricted_device_groups` | SID_AND_ATTRIBUTES[]? | Filtered device groups for restricted tokens. |

## 3.2.2.13 Confinement (fixed)

| Field | Type | Description |
|---|---|---|
| `confinement_sid` | SID? | Places the token in a default-deny sandbox; null means unconfined. When set, AccessCheck switches to default-deny and access requires an explicit grant to this SID or to a SID present in `confinement_capabilities`. |
| `confinement_capabilities` | SID_AND_ATTRIBUTES[] | Declared access capabilities for a confined process, empty if none. The `attributes` field is carried for wire-format uniformity only: AccessCheck treats capability membership as presence-based and consults neither `SE_GROUP_ENABLED` nor `SE_GROUP_USE_FOR_DENY_ONLY` when matching confinement SIDs. |
| `isolation_boundary` | bool | Adds namespace filtering on top of confinement, making objects outside the boundary invisible rather than merely denied. Requires `confinement_sid`. Settable at creation but not enforced. |
| `confinement_exempt` | bool | Escape hatch: confinement restrictions are not evaluated at all. |

`ALL_APPLICATION_PACKAGES` participates only when it is present in
`confinement_capabilities`. KACS never synthesises it, and equally
never rejects an otherwise valid confined token merely because it is
present — strict confinement is expressed by the caller omitting it.
Deciding which capabilities a package token receives is authd's job
and policy tooling's, not the kernel's.

## 3.2.2.14 Audit (fixed)

| Field | Type | Description |
|---|---|---|
| `audit_policy` | u32 | Per-token audit overrides as a bitmask, fixed at creation — no adjustment operation exists. |

| Flag | Value | Description |
|---|---|---|
| `OBJECT_ACCESS_SUCCESS` | 0x0001 | Audit successful object access. |
| `OBJECT_ACCESS_FAILURE` | 0x0002 | Audit failed object access. |
| `PRIVILEGE_USE_SUCCESS` | 0x0004 | Audit successful privilege exercises: the privilege contributed requested bits that survive into the final granted result. |
| `PRIVILEGE_USE_FAILURE` | 0x0008 | Audit failed privilege exercises: the privilege contributed requested bits during evaluation, but they do not survive into the final result. |

The policy is additive. It forces audit events that system-wide policy
would not generate, and cannot suppress events that system-wide policy
requires. It follows impersonation: when service A impersonates client
B and B's token has a category enabled, operations during
impersonation are audited under B's identity. The creation default is
0.

## 3.2.2.15 Credential projection (fixed)

| Field | Description |
|---|---|
| `projected_uid` | Linux UID for the user SID, precomputed by authd when the token is minted (PSPU §2); 65534 for the anonymous identity. |
| `projected_gid` | Linux primary GID, precomputed the same way; 65534 for the anonymous identity. |
| `projected_supplementary_gids` | Linux supplementary GIDs, one per group SID, precomputed the same way. |

authd computes these at token creation and they are stored on the
token; KACS never resolves a SID-to-UID mapping at runtime. Projection
reflects all groups regardless of enabled state, so adjusting groups
does not trigger recalculation. §3.10 covers how the projection is
used.

## 3.2.2.16 Token security (adjustable)

| Field | Description |
|---|---|
| `security_descriptor` | The token's own SD, controlling who may query, adjust, duplicate, or impersonate it. |

## 3.2.2.17 Internal

| Field | Description |
|---|---|
| `refcount` | Reference count; the token is freed when the last reference drops. Not exposed to userspace. |

---

# 3.2.3 Token Lifecycle

_Peios / Advanced Peios / PKM / KACS / Tokens_

> How tokens follow a process through fork, clone and exec — NEW_PROCESS_MIN, self-installation, bootstrap tokens and external tokens.

## 3.2.3.1 Fork and clone

Every process and thread creation path goes through `clone()`, and
KACS branches on `CLONE_THREAD`.

**Without `CLONE_THREAD`** — fork or vfork — a new process is created
and the child receives an independent deep copy of the parent's
primary token. If the parent is impersonating, the impersonation token
is not inherited: the child's effective credential is set from
`real_cred`. After the fork, mutations to either token are invisible
to the other.

**With `CLONE_THREAD`** a new thread is created. Threads share the
parent's `real_cred` by reference and therefore share one primary
token object, so a privilege adjustment on it is visible to every
thread. Each thread keeps independent impersonation state through its
own `cred`. If the cloning thread is impersonating at the moment of
the clone, the impersonation token does not become the new thread's
primary or effective identity — the new thread starts with the shared
primary token as both, and may impersonate independently later.

## 3.2.3.2 Exec

The primary token survives `execve()` unchanged, with the single
exception of `NEW_PROCESS_MIN` below. If the calling thread is
impersonating, impersonation is reverted before the new program runs;
a new program always starts with the primary token as its effective
identity.

Token assignment for services happens between fork and exec: peinit
forks, installs the service's token on the child, and the child then
execs the service binary.

### 3.2.3.2.1 NEW_PROCESS_MIN

When a token's `mandatory_policy` includes `NEW_PROCESS_MIN`, the
kernel replaces the primary token at exec time if the executable
carries a lower integrity label.

1. The executable's integrity label is read from the mandatory label
   ACE in its descriptor's SACL. A file with no label is treated as
   Medium.
2. If the file's level is lower than the token's, a new token is
   created following DuplicateToken semantics — new `token_id`, new
   `token_guid`, `modified_id` initialised to the new `token_id`,
   `elevation_type` reset to Default — with `integrity_level` set to
   the file's label. Every other field is copied from the source, and
   the original token is dropped.
3. If the file's level is greater than or equal to the token's,
   nothing happens and the token survives exec unchanged.

The mechanism only ever lowers integrity, so a child's level is always
at most its parent's. The flag itself is immutable on the token, which
is what prevents a process from opting out before exec.

## 3.2.3.3 Self-installing a primary token

`KACS_IOC_INSTALL` commits a new primary token on the calling process.
The operation is process-wide: the kernel replaces the primary token
for the entire thread group, not just the calling thread.

A thread that is not impersonating switches both `real_cred` and
`cred` to the new primary token. A thread that is impersonating has
only `real_cred` replaced, and its active impersonation stays in
`cred` until it reverts or execs — so reverting after an install
restores the thread to the *new* primary token, not the old one.

The calling thread installs immediately. Sibling threads converge in
their own context through queued in-kernel credential work, with no
atomic all-threads-at-once swap: during a brief transition window some
siblings may still observe the old primary token until they run their
queued work. No completion barrier is exposed to the caller.

If the installed token's user SID differs from the outgoing primary
token's, the process security descriptor is regenerated from the
default template for the new token. Otherwise the existing process
descriptor is preserved.

## 3.2.3.4 Bootstrap tokens

Two tokens are created by PKM during kernel initialisation, before any
userspace process exists. Neither involves a syscall — the kernel
allocates the objects directly.

The **SYSTEM token** is hardcoded with user SID `S-1-5-18` (Local
System); groups `S-1-5-32-544` (BUILTIN\Administrators), `S-1-1-0`
(Everyone), `S-1-5-11` (Authenticated Users) and other well-known
SIDs; every defined privilege present and enabled; integrity level
System; token type Primary; elevation type Default; token source
`PeiosKrn`; projected UID 0; and `auth_id` set to `SYSTEM_LUID`. It is
assigned to the kernel's init task and inherited by PID 1 at exec.

The **Anonymous token** is a global singleton with user SID `S-1-5-7`;
Everyone (`S-1-1-0`) as its only group; no privileges; integrity level
Untrusted; logon type Network; token type Impersonation; impersonation
level Anonymous; elevation type Default; token source `PeiosKrn`; and
`auth_id` set to `ANONYMOUS_LOGON_LUID`. It is effectively immutable,
having no privileges and no groups to adjust. `kacs_impersonate_peer`
at Anonymous level references this global object, whereas
`KACS_IOC_DUPLICATE` targeting Anonymous level creates a fresh
independent token of the same shape, because the DuplicateToken
contract requires a new object.

| LUID | Value | Description |
|---|---|---|
| `SYSTEM_LUID` | 999 (0x3E7) | The SYSTEM LogonSession, created at kernel init. |
| `ANONYMOUS_LOGON_LUID` | 998 (0x3E6) | The Anonymous LogonSession, created at kernel init for Anonymous impersonation tokens. |

LogonSessions created dynamically by authd receive auto-generated
LUIDs starting at 1000; 999 and 998 are never assigned dynamically.

The SYSTEM token carries `SeBackupPrivilege` and `SeRestorePrivilege`
present and enabled at boot. FACS passes backup and restore intent
flags into AccessCheck, which grants read and write regardless of file
DACLs — subject still to PIP. Once peinit has launched the TCB
services and early boot is complete, it disables these on child
service tokens through FilterToken.

## 3.2.3.5 External token replacement

A privileged process being able to replace the primary token on
another running process — peinit downgrading a pre-authd service from
SYSTEM to a purpose-built token, for instance — is designed but **not
built**. Nothing in the kernel implements it today.

The design uses `task_work_add()` to queue a credential swap on each
task in the target thread group, each task executing the swap in its
own context to preserve RCU safety, with an impersonating thread
having only `real_cred` replaced and its impersonation left intact.
The gates would be `SeAssignPrimaryTokenPrivilege` on the caller's
real token, `TOKEN_ASSIGN_PRIMARY` (0x0001) on the token fd,
`PROCESS_SET_INFORMATION` on the target process's descriptor, and two
constraints — the new token's user SID matching the target's current
one, and the new token belonging to the same LogonSession — both
bypassed by `SeTcbPrivilege`. The process descriptor gate puts the
target's owner in control of who can change its identity, while the
SID and LogonSession constraints stop a non-TCB holder of
`SeAssignPrimaryTokenPrivilege` assigning an arbitrary token to a
process it can reach. `SeTcbPrivilege` bypassing both is how peinit
would assign tokens with different user SIDs and LogonSessions to
child services. Per-thread queuing would leave a brief window with
some threads on the new token and some on the old, which is acceptable
only because replacement is always a downgrade.

In its absence, the mitigation for pre-authd services is privilege
removal: peinit uses FilterToken to copy the SYSTEM token with the
dangerous privileges permanently deleted and assigns that filtered
token to the service at launch. The service keeps the SYSTEM SID but
permanently lacks the ability to exercise those privileges.

---

# 3.2.4 Token Creation

_Peios / Advanced Peios / PKM / KACS / Tokens_

> The three operations that produce a token — CreateToken from nothing, DuplicateToken from another, FilterToken for a restricted one.

Three operations create tokens, each for a different purpose:
CreateToken mints one from nothing, DuplicateToken copies one, and
FilterToken produces a strictly weaker copy.

## 3.2.4.1 CreateToken

Mints a new token from scratch. The caller supplies the
security-meaningful content; the kernel generates the internal
bookkeeping and validates the structural invariants. The operation is
gated by `SeCreateTokenPrivilege`.

The caller supplies `user_sid`, `groups` with their attributes,
privileges as `privs_present` plus `privs_enabled`, `owner_sid_index`,
`primary_group_index`, `default_dacl`, `integrity_level`,
`mandatory_policy`, `token_type`, `impersonation_level`, `auth_id`
referencing an existing LogonSession, `expiration` (0 for none),
`audit_policy`, `source` as a name plus LUID, `user_claims`,
`device_claims`, `lcs_scope_guids`, `lcs_private_layers`,
`device_groups`, `restricted_sids`, `restricted_device_groups`,
`confinement_sid`, `confinement_capabilities`, `confinement_exempt`,
`isolation_boundary`, `write_restricted`, `user_deny_only`,
`projected_uid`, `projected_gid`, `projected_supplementary_gids`,
`origin`, and `interactivity_scope`. The wire format is in §3.A.

Including the well-known implicit groups is the caller's
responsibility — Everyone (`S-1-1-0`), Authenticated Users
(`S-1-5-11`), and whatever else the principal's authentication context
implies, such as `S-1-5-4` Interactive, `S-1-5-6` Service, or
`S-1-5-15` This Organization. The kernel injects none of these. The
logon SID is the sole kernel-generated group.

The kernel generates `token_id` as a LUID, `token_guid`, `modified_id`
initialised to `token_id`, `created_at` as the current time,
`elevation_type` always Default, the token's own default security
descriptor (§3.2.7), and `logon_sid`, derived from the LogonSession ID
as `S-1-5-5-{id >> 32}-{id & 0xFFFFFFFF}`.

The logon SID is injected into the groups array carrying
`SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED |
SE_GROUP_LOGON_ID`, appended after the caller's groups. Callers do not
include it themselves. Because the injected entry is appended,
`owner_sid_index` and `primary_group_index` are interpreted relative
to the caller-supplied groups — 0 for the user SID, 1..N for the
caller's groups — and not against the array with the logon SID in it.

Validation covers, in turn: that the caller holds
`SeCreateTokenPrivilege`; that every SID is structurally well-formed;
that the owner SID is the user SID or a group carrying
`SE_GROUP_OWNER`, resolved to `owner_sid_index`; that the primary
group SID is the user SID or a group on the token, resolved to
`primary_group_index`; that `auth_id` references an existing
LogonSession; that a Primary token carries impersonation level
Anonymous; that `user_deny_only` is true whenever `write_restricted`
is; that `confinement_sid` is present whenever `isolation_boundary`
is; that the wire format's `elevation_type` field is 0, since the
kernel always sets Default itself; and that the caller's group count
plus the injected logon SID fits the 1024-entry limit.

The optional LCS registry credential extension, when present, has to
use the version and layout of §3.A, and carries at most 256 scope
GUIDs and at most 256 private layer names, with no nil scope GUID, no
duplicate scope GUIDs, no empty or overlong layer names, and no
duplicate layer names under LCS case-insensitive matching. Malformed
LCS credentials fail the whole call closed.

The kernel does not authenticate the user, look up SIDs in the
directory, resolve SID-to-UID mappings, or check that the principal
exists at all. Holding `SeCreateTokenPrivilege` is what makes the
caller trusted, and that trust is total.

The call returns a token file descriptor. Since CreateToken takes no
desired-access parameter, the returned handle always carries a cached
access mask of `TOKEN_ALL_ACCESS`.

## 3.2.4.2 DuplicateToken

Creates an independent copy of an existing token, requiring
`TOKEN_DUPLICATE` access on the source.

Two things may change during duplication. The **token type** may go
from primary to impersonation or the reverse; duplicating to Primary
forces `impersonation_level` to Anonymous. The **impersonation level**
may be chosen freely when the source is a Primary token, but when the
source is itself an impersonation token the new level has to be equal
to or lower than the source's — an Identification-level token cannot
be duplicated up to Impersonation or Delegation.

On the new token, `token_id` and `token_guid` are fresh, `modified_id`
is initialised to the new `token_id`, and `elevation_type` resets to
Default because the copy belongs to no linked pair. `token_type` and
`impersonation_level` are as the caller specified, within the rules
above. The token's own descriptor is a fresh default (§3.2.7): no
custom descriptor can be supplied at duplication time, and changing it
afterwards means using `WRITE_DAC` on the new handle.

Everything else is copied from the source: `user_sid`,
`user_deny_only`, `logon_sid`; `groups` with all per-group attributes;
`restricted_sids` and `write_restricted`; the privilege present,
enabled, enabled-by-default **and used** states; `integrity_level` and
`mandatory_policy`; `auth_id`, `origin`, `source`, `created_at`,
`expiration`, `audit_policy`; `interactivity_scope`; `default_dacl`,
`owner_sid_index`, `primary_group_index`; `user_claims`,
`device_claims`, `device_groups`, `restricted_device_groups`;
`lcs_scope_guids` and `lcs_private_layers`; `confinement_sid`,
`confinement_capabilities`, `confinement_exempt`; and the three
projection fields.

One target is not a copy at all. Duplicating to **Impersonation at
Anonymous level** discards the source entirely and returns a fresh
token of the boot Anonymous shape — user SID `S-1-5-7`, Everyone as
its only group, no privileges, Untrusted integrity, and LogonSession
998 rather than the source's. None of the copied-field rules above
apply to it. Assuming Anonymous is an identity boundary rather than a
level change, so the operation constructs the minimal identity instead
of narrowing the caller's.

The original token is unaffected.

## 3.2.4.3 FilterToken

Creates a restricted copy, requiring `TOKEN_DUPLICATE` access on the
source. Filtering only ever weakens: there is no parameter that grants
anything.

It can **remove privileges**, deleting them permanently from the new
token by clearing them from the present, enabled, and
enabled-by-default states at once. It can **set groups to deny-only**,
giving them `SE_GROUP_USE_FOR_DENY_ONLY` so they block access through
deny ACEs but never grant it through allow ACEs — permanently, with no
way back. It can **add restricted SIDs**, a secondary list that makes
AccessCheck evaluate the DACL twice, granting access only when the
normal SIDs and the restricted SIDs independently both pass. And it
can **enable write-restricted mode**, limiting that second evaluation
to write operations so reads use the normal list alone; enabling it
forces `user_deny_only` true on the new token.

Input validation is all-or-nothing — a single malformed entry means no
token is created. The deny-only list uses zero-based group indices
into the source's group array, and a duplicate or out-of-range index
is invalid. The restricting SID blob has to parse exactly as the
declared packed SID list, with no truncated or trailing bytes. If the
source token is already restricted and the intersection of its
restricted SID list with the supplied list is empty, the request is
invalid and nothing is created.

On the new token, `token_id` and `token_guid` are fresh, `modified_id`
is initialised to the new `token_id`, and `elevation_type` resets to
Default. The privilege states are the source's modified by the removal
list, except that **`used` resets to 0** — unlike duplication, which
carries it across. `groups` keeps the source's SIDs with attributes
modified per the deny-only list, adding and removing nothing.
`restricted_sids` is the supplied list, or its intersection with the
source's when the source was already restricted. `write_restricted` is
sticky: set if requested or if the source had it. `user_deny_only` is
true when write-restricted is enabled and otherwise copied.
`user_sid`, `logon_sid`, `integrity_level`, `mandatory_policy`,
`token_type` and `impersonation_level` are copied, as are `auth_id`,
`origin`, `source`, `created_at`, `expiration`, `audit_policy`,
`default_dacl`, `owner_sid_index`, `primary_group_index`, the claims
and device group arrays, the LCS credentials, the confinement fields,
and the projection fields. The token's descriptor is a fresh default.

---

# 3.2.5 Token Adjustment

_Peios / Advanced Peios / PKM / KACS / Tokens_

> What can be changed on a live token — privileges, groups, interactivity scope and object-creation defaults — and what cannot.

A live token's privileges, groups, default object-creation metadata,
and interactive session metadata can be adjusted at runtime. These are
distinct operations with separate access rights and constraint models.
All of them mutate the token object in place using atomic operations,
all become visible immediately to every thread sharing the token, and
all bump `modified_id`.

## 3.2.5.1 AdjustPrivileges

Requires `TOKEN_ADJUST_PRIVILEGES` on the token, and has three modes.

**Enable and disable** flips individual bits in `privileges_enabled`
for privileges present on the token. A privilege that is not present
cannot be enabled, and disabling one that is already absent is a
no-op. The operation activates existing privileges; it never grants
new ones.

**Reset to defaults** restores `privileges_enabled` to match
`privileges_enabled_by_default`, returning every privilege to its
creation-time state in one operation. It is encoded as a single
`kacs_priv_entry` with `luid = 0` and
`attributes = KACS_PRIV_RESET_ALL_DEFAULTS`. The reset touches only
`privileges_enabled`: it does not restore privileges that were removed
from `privileges_present`.

**Remove** permanently deletes a privilege, clearing its bit in
`privileges_present`, `privileges_enabled`, and
`privileges_enabled_by_default` together. Removing an already-absent
privilege is a no-op. The deletion is irreversible; nothing re-adds a
privilege to a token.

Duplicate privilege indices in one request are invalid. The kernel
validates every entry before applying any change, so an invalid entry
fails the whole operation with no state change at all. The caller
receives a report of each adjusted privilege's previous state.

## 3.2.5.2 AdjustGroups

Requires `TOKEN_ADJUST_GROUPS` on the token, and has two modes.

**Enable and disable** flips `SE_GROUP_ENABLED` on individual groups,
within two constraints. A group carrying `SE_GROUP_MANDATORY`,
`SE_GROUP_USE_FOR_DENY_ONLY`, or `SE_GROUP_LOGON_ID` cannot be
adjusted **in either direction** — not enabled, not disabled, whatever
its current state; naming one in a request fails the whole call. And
the user SID, when it appears in the group list, cannot be disabled,
which is the one constraint that is direction-scoped.

**Reset to defaults** restores every group to its creation-time
enabled state. It restores only that state — it does not clear
`SE_GROUP_USE_FOR_DENY_ONLY` from a group that FilterToken marked
deny-only afterwards.

Groups are addressed by zero-based index into the token's groups
array. A count of 0 is invalid, as is a count above 1024, as are
duplicate indices in one request. Reset is encoded as a single
`kacs_group_entry` of `{ index = 0xFFFFFFFF, enable = 0 }`.

The caller receives the previous enabled state of every group as a
1024-bit mask encoded as sixteen 64-bit words in ascending order: word
0 covers group indices 0–63, word 1 covers 64–127, and bit `i % 64` of
word `i / 64` corresponds to group index `i`. Since group arrays cap
at 1024 entries, the mask is complete for any valid token.

## 3.2.5.3 AdjustInteractivityScope

Requires `TOKEN_ADJUST_INTERACTIVITY_SCOPE` on the token and
`SeTcbPrivilege` on the caller's real token. It changes the token's
`interactivity_scope` to a new `u32` and nothing else: the field is
metadata, and changing it grants or removes no privilege, group,
access right, label, claim, default owner, default primary group,
default DACL, or any other authorization state.

## 3.2.5.4 AdjustDefault

Requires `TOKEN_ADJUST_DEFAULT` on the token, and covers three fields.

The **default DACL** applied to new objects is replaced by an RCU
pointer swap, with the old DACL freed after a grace period. The
**owner SID index** changes which SID becomes the default owner of new
objects, and has to reference the user SID or a group carrying
`SE_GROUP_OWNER`. The **primary group index** changes the default
primary group, and has to reference the user SID or any group SID on
the token. Both index updates are atomic.

All three affect future object creation only; existing objects are
untouched. None can escalate anything, because the caller is choosing
among SIDs already on their own token.

`audit_policy` is fixed at creation, and no adjustment operation
changes it.

---

# 3.2.6 Linked Tokens and Elevation

_Peios / Advanced Peios / PKM / KACS / Tokens_

> Two linked tokens for one principal — what the kernel does with the link, what it deliberately leaves to userspace, and the pair's lifecycle.

At logon, authd may create two tokens for one principal and link them.
The **elevated token**, `elevation_type = Full`, carries the user's
complete identity: every group active, every assigned privilege
present and enabled. The **filtered token**,
`elevation_type = Limited`, carries the same user SID with
administrative groups set to deny-only and dangerous privileges
stripped, produced from the elevated token by FilterToken.

Both tokens belong to the same LogonSession, are both primary tokens,
and carry the same user SID. The filtered token is installed as the
LogonSession's default; the elevated token exists but is not directly
reachable by unprivileged processes.

A token never assigned a linked-pair role has
`elevation_type = Default`. Once `KACS_IOC_LINK_TOKENS` sets a token
object to Full or Limited, that role is sticky on that object. If the
pair is later replaced or destroyed the token has no active partner
and `KACS_IOC_GET_LINKED_TOKEN` returns an error, but the token goes
on reporting its last assigned elevation type.

## 3.2.6.1 What KACS does

KACS provides pairing, storage, and query restriction — three
mechanisms, no policy.

**Linked pair association** registers the pair on the LogonSession, so
given either token the system can retrieve its partner. The pairing
lives at the LogonSession level and is not stored on the token
objects.

Establishing a pair is a TCB operation: the caller holds
`SeTcbPrivilege` on its **primary** token — an impersonating thread's
effective token does not satisfy it — and holds `TOKEN_DUPLICATE` on
*both* of the token handles being linked. The two handles have to name
distinct token objects, and both have to belong to the LogonSession
named in the request, which itself has to be published. The ioctl
ignores the handle it was issued on entirely; only the two named
handles matter.

**Elevation type classification** puts `elevation_type` on each token
so a consumer can tell which side it is holding.

**Identification-level query restriction** governs what an unprivileged
caller gets when it queries a token's partner: a deep clone at
Identification impersonation level. The clone follows DuplicateToken
semantics — a new token object, a new `token_id`, `modified_id`
initialised to it, a fresh default descriptor — except that it
preserves the partner's `elevation_type`, and it is always returned
through a `TOKEN_QUERY`-only handle. The caller can inspect the
elevated token but cannot use it for an access decision. A caller
holding `SeTcbPrivilege` receives a full handle to the actual linked
token instead.

Returning a copy through `TOKEN_QUERY` is a deliberate exception to
the normal access-right model, where `TOKEN_DUPLICATE` would be
expected. It holds because the returned copy is always
Identification-level: functionally a query result rather than a usable
token.

## 3.2.6.2 What KACS does not do

The elevation decision itself — whether this user should be allowed to
use their elevated token right now — is entirely authd's. KACS does
not gate elevation, verify credentials, or display prompts. It stores
the pair and restricts unprivileged access to it.

Beyond enforcing the LogonSession, token-type and same-user
invariants, KACS does not verify that the filtered token really is a
FilterToken-derived reduction of the elevated one. That correspondence
is authd's to get right.

## 3.2.6.3 Lifecycle

Both tokens in a pair share a LogonSession, and that session is not
destroyed while any token fd, credential, pair slot, or other
reference keeps one of its token objects live. When the last external
reference is released and only the linked-pair's own references
remain, KACS destroys the LogonSession, removes the linkage, and drops
the pair's references to both tokens. After that cleanup no token
object from the session remains live purely because it was linked.

Stale-role tokens can exist before final destruction — for instance
when `KACS_IOC_LINK_TOKENS` replaces a LogonSession's active pair
while an old token object is still held by an fd or credential. Fork
produces them too: the deep copy a child receives preserves the
parent's elevation type, so a forked child can hold a Full or Limited
token that was never linked to anything. Once a
token is no longer the active member of the pair, querying its linked
token returns an error, because the partner relationship no longer
exists for it. Such survivors keep their sticky Full or Limited
elevation type: they are stale-role tokens with no active partner, not
Default tokens.

---

# 3.2.7 LogonSessions and Revocation

_Peios / Advanced Peios / PKM / KACS / Tokens_

> The LUID-identified object every token references — how sessions are created, how they expire, and how revocation propagates.

## 3.2.7.1 LogonSessions

A LogonSession is a lightweight kernel object identified by a LUID,
carried on tokens as `auth_id`. Every token references one.

authd creates a LogonSession through a KACS syscall at authentication
time, before creating the token. The object holds the LogonSession ID,
the logon type (Interactive, Network, Service, and so on), the user
SID, the authentication package name such as `Kerberos` or
`Negotiate`, and a creation timestamp. The logon SID, `S-1-5-5-X-Y`,
is derived from the LogonSession ID. Several tokens may share one
session — linked pairs, and tokens derived by duplication.

When the last token referencing a session is freed, the kernel
destroys the session object and emits a `logon-session-destroyed`
event through KMES. authd subscribes to those events and uses them to
clean up associated credentials such as cached Kerberos tickets.

There is one rollback path for the case where authd creates a session
but no token ever becomes live for it:
`kacs_destroy_empty_logon_session`, which requires `SeTcbPrivilege`
and succeeds only when the session exists, has zero live tokens, has
no linked-token state, and has no other in-flight kernel references.
On success it destroys the object and emits the same
`logon-session-destroyed` event as normal cleanup. A nonexistent
session fails with `-ENOENT`; one with any live token, linked-token
state, or in-flight reference fails with `-EBUSY`.

A second enumeration surface exists alongside `/proc`:
`/sys/kernel/security/kacs/sessions` lists every live session, one
line each, giving the session ID, user SID, logon type, authentication
package, and creation time. Reading it is access-checked against a
synthetic descriptor granting read to SYSTEM and the creator, and is
PIP-checked.

AccessCheck never consults `auth_id`, and the logon SID influences a
decision only because it is materialised as an ordinary group SID on
the token. Two enforcement decisions elsewhere in the kernel do read
session state, though: installing a primary token denies a non-TCB
caller whose target token belongs to a different LogonSession, and the
`CAP_SYS_BOOT` mapping selects between `SeShutdownPrivilege` and
`SeRemoteShutdownPrivilege` by inspecting the session's logon type. `interactivity_scope` is
metadata in the same way: the kernel stores it and returns it on
query, and no kernel security mechanism evaluates it.

## 3.2.7.2 Expiration

The `expiration` field carries a timestamp, and AccessCheck does not
enforce it. It is informational.

Token lifetime is governed by reference counting instead: a token
exists as long as at least one reference — a process credential or an
open file descriptor — exists.

## 3.2.7.3 Revocation

KACS has no token revocation primitive. There is no "invalidate token
X" syscall, and no syscall destroys a LogonSession while tokens still
reference it. `kacs_destroy_empty_logon_session` is only authd's
rollback for a session that never acquired live tokens.

Terminating a LogonSession is therefore userspace coordination:

1. authd decides a session has to end — an admin request, a security
   incident, an account deletion, or a user logging off.
2. authd enumerates processes whose tokens carry the target `auth_id`
   or `interactivity_scope` by walking `/proc/*/token`, opening each
   node's query-only inspection handle, and reading `TokenStatistics`,
   which includes `auth_id`. No dedicated enumeration syscall exists
   or is needed.
3. authd requests termination — through peinit for supervised
   services, through signals for user processes.
4. The processes terminate, dropping their token references.
5. The last reference drops and the session object is cleaned up.

Token file descriptors can be passed between processes over IPC, so a
reference held by a process outside the target session survives that
session's process termination. authd has to account for this when
enumerating token holders — the walk finds processes running under the
session, not every process holding one of its tokens.

Kernel-side invalidation — a dead flag on the LogonSession object
checked during AccessCheck, so that access checks against its tokens
fail immediately — is not implemented.

---

# 3.2.8 Token Access Rights

_Peios / Advanced Peios / PKM / KACS / Tokens_

> Tokens are securable objects — obtaining a token file descriptor, the token-specific rights, the generic mapping and the default descriptor.

Tokens are securable objects: each has its own security descriptor,
and reaching a token means passing an AccessCheck against it.

## 3.2.8.1 Obtaining a token file descriptor

**Opening directly.** A syscall takes a pidfd — not a raw PID — and a
desired access mask. The kernel finds the target's
primary token, evaluates the caller's token against that token's
descriptor, and returns a token fd with the granted mask cached on it.
A separate variant opens a thread's impersonation token. Opening
another process's token additionally requires
`PROCESS_QUERY_INFORMATION` on the target process's descriptor.

`kacs_open_peer_token` is the exception: it takes no desired-access
mask, and the fd it returns always carries the fixed rights
`TOKEN_QUERY | TOKEN_IMPERSONATE`.

**Receiving over IPC.** A token fd can be passed over a Unix socket
with `SCM_RIGHTS`. What the recipient may do is bounded by the mask
cached on the fd when it was originally opened, not by the recipient's
own identity.

**Implicit self-access.** A thread has implicit access to its own
effective token for query operations, but this is not a kernel bypass.
It follows from the default token descriptor, which grants
`TOKEN_QUERY` and the adjustment rights to the token's own user SID.
The AccessCheck still runs; it simply succeeds while the descriptor
continues to grant the right. Explicitly mutating a token's own
descriptor later can revoke self-query by removing that grant.

## 3.2.8.2 Token-specific rights

| Right | Value | Grants |
|---|---|---|
| `TOKEN_ASSIGN_PRIMARY` | 0x0001 | Install as a process's primary token. Also requires `SeAssignPrimaryTokenPrivilege` on the caller's token. |
| `TOKEN_DUPLICATE` | 0x0002 | Duplicate the token, or create a restricted copy with FilterToken. |
| `TOKEN_IMPERSONATE` | 0x0004 | Install as a thread's impersonation token. |
| `TOKEN_QUERY` | 0x0008 | Read token information: SIDs, groups, privileges, integrity, claims, source, statistics, elevation type. |
| `TOKEN_ADJUST_PRIVILEGES` | 0x0020 | Enable, disable, or permanently remove privileges. |
| `TOKEN_ADJUST_GROUPS` | 0x0040 | Enable or disable groups. |
| `TOKEN_ADJUST_DEFAULT` | 0x0080 | Change the default DACL, owner SID, and primary group SID. |
| `TOKEN_ADJUST_INTERACTIVITY_SCOPE` | 0x0100 | Change the interactivity scope. Also requires `SeTcbPrivilege`. |

Bit 0x0010, `TOKEN_QUERY_SOURCE`, is subsumed by `TOKEN_QUERY`: a
holder of 0x0008 can query source information too. The bit is not
reused for anything else, for format compatibility with MS-DTYP.

`TOKEN_ALL_ACCESS` is 0x000F01FF — the union of the token-specific
rights with `STANDARD_RIGHTS_REQUIRED`
(`DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER`, 0x000F0000). The
named rights alone OR to 0x01EF; the reserved `TOKEN_QUERY_SOURCE` bit
adds 0x0010 to reach 0x01FF.

## 3.2.8.3 Generic mapping

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `TOKEN_QUERY \| READ_CONTROL` |
| `GENERIC_WRITE` | `TOKEN_ADJUST_PRIVILEGES \| TOKEN_ADJUST_GROUPS \| TOKEN_ADJUST_DEFAULT \| WRITE_DAC` |
| `GENERIC_EXECUTE` | `TOKEN_IMPERSONATE` |
| `GENERIC_ALL` | `TOKEN_ALL_ACCESS` (0x000F01FF) |

## 3.2.8.4 Standard rights

`READ_CONTROL` reads the token's own descriptor, `WRITE_DAC` modifies
its DACL, and `WRITE_OWNER` changes its owner. `DELETE` has no
practical effect on a token and is present only for uniformity across
standard rights.

## 3.2.8.5 The default token descriptor

A newly created token receives a descriptor owned by the creating
process's user SID, with a DACL granting:

- the token's own user SID `TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES |
  TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT`;
- the creator `TOKEN_ALL_ACCESS`;
- SYSTEM (`S-1-5-18`) `TOKEN_ALL_ACCESS`.

Self-access is deliberately limited to the adjustment operations that
cannot escalate. `TOKEN_DUPLICATE`, `TOKEN_IMPERSONATE`, and
`WRITE_DAC` are not granted to the token's own subject.

That limit needs protecting in the case where the creator and the
token's own user SID are the same, which would otherwise hand the
subject `TOKEN_ALL_ACCESS` through the creator ACE. When the two SIDs
are identical the creator ACE is omitted — and the descriptor also
gains a non-inherit-only `OWNER RIGHTS` ACE suppressing the owner's
implicit `READ_CONTROL | WRITE_DAC` grant while preserving
`READ_CONTROL`. Without it, owner-implicit `WRITE_DAC` would let the
subject rewrite its own DACL and reintroduce exactly the escalation
that omitting the creator ACE was meant to close.

## 3.2.8.6 Check-at-open

The check-at-open model applies to tokens exactly as it does to files,
for the token-specific rights. AccessCheck runs once, when the token
fd is obtained; the granted mask is cached on the fd; and each of the
token ioctls verifies against that cached mask with no re-evaluation.

The standard rights are the exception. Reading and writing a token's
own descriptor passes no cached mask at all and runs a **live**
AccessCheck on every call, so `READ_CONTROL`, `WRITE_DAC` and
`WRITE_OWNER` are re-evaluated per operation rather than snapshotted
at open. A descriptor change therefore takes effect immediately for
those three rights, while already-opened handles keep their cached
token-specific rights.

---

# 3.3.1 The PSB Model

_Peios / Advanced Peios / PKM / KACS / The Process Security Block_

> The per-process structure describing what a process is, independent of the principal running it.

The Process Security Block is a per-process structure carrying
properties that describe **what the process is**, independent of the
principal running it.

Its PIP identity fields, `pip_type` and `pip_trust`, are determined by
the binary loaded at exec (§3.6). Its other fields — process
mitigations and process restrictions — are process policy, set by
whoever launches the process. Neither category is token identity, and
neither is determined by the principal.

Keeping the PSB separate from the token is the point. The token
describes identity and travels with impersonation: when a thread
impersonates a client, the effective token changes. The PSB describes
the process, and impersonation never touches it.

> The PSB is never affected by impersonation. Impersonation changes
> who the thread is acting as. It does not change what the process is.

The canonical PSB reference lives on the task's LSM security blob,
`task_struct->security`, separate from the credential that holds the
token pointer. A mirrored, non-authoritative reference is also kept in
credential security blobs, for the benefit of Linux hooks that receive
only a credential. For any task-attached credential that mirror refers
to the same PSB as the task blob, and it never defines a different
process identity.

Credentials are swapped during impersonation, and that swap may change
the credential's token pointer — but the canonical PSB is untouched
and any credential-level mirror continues to refer to it.

---

# 3.3.2 PSB Fields

_Peios / Advanced Peios / PKM / KACS / The Process Security Block_

> Every PSB field — the process GUID fixed at fork, the protection fields set at exec, the one-way mitigations and the UI access flag.

## 3.3.2.1 Process identity (fixed at fork)

| Field | Type | Description |
|---|---|---|
| `process_guid` | UUID | 128-bit identifier for this process instance, generated by the kernel at fork and immutable for the process's lifetime. It is **not** copied from the parent — every process receives a new one. Used by KMES and kernel-internal consumers for identity stamping and event correlation. |

The process GUID is distinct from the PID. PIDs are recycled; process
GUIDs are unique within a boot, and globally unique in practice. It is
the stable correlation key that lets KMES attribute events to a
process across its whole lifetime.

## 3.3.2.2 Protection (set at exec, fixed)

| Field | Type | Description |
|---|---|---|
| `pip_type` | u32 | Process Integrity Protection type. Determined by the binary's cryptographic signature at exec. |
| `pip_trust` | uint | Trust tier within a PIP type; higher values can reach lower ones. Determined by the signer's identity. |

PIP fields are signing-based. At exec the kernel verifies the binary's
signature and derives both fields from the signer, using the algorithm
and key model of §3.6.

Both are plain unsigned integers rather than enumerations, and are
compared numerically. Three type values are conventional — None (0),
Protected (512) and Isolated (1024) — but only two are producible: the
key table validator accepts a key only at exactly Protected with
`PeiosTcb` trust (8192) and rejects the whole table otherwise, so
**Isolated is unreachable** and a signed binary is always
Protected/8192. Neither None nor Isolated is defined as a named
constant in the public ABI at all. The parent process cannot influence the
determination at all — even a compromised peinit running as SYSTEM
cannot forge PIP protection for an unsigned binary. The public
verification key is compiled into the kernel image, and the kernel
only ever verifies; it never signs.

This is a deliberate departure from MS-DTYP, where the parent sets a
protection level at process creation and the kernel validates the
binary's signature against it. Peios removes the parent-controlled
half entirely: one input, one answer.

## 3.3.2.3 Process mitigations (one-way)

| Field | Description |
|---|---|
| `lsv` | **Library Signature Verification.** Only signed shared libraries load. When the process has `pip_type != None` the library's trust level has to be at or above the process's PIP trust; when `pip_type = None` any valid signature suffices and trust is not compared. |
| `wxp` | **Write-XOR-Execute Protection.** No page is simultaneously writable and executable; W+X mappings and transitions between writable and executable are rejected. |
| `tlp` | **Trusted Library Paths.** Shared libraries load only from approved directory prefixes. Weaker than LSV, since it trusts the path rather than the binary. |
| `cfif` | **Forward-Edge Control Flow Integrity.** Hardware indirect-branch tracking — Intel IBT, ARM BTI — is locked on and cannot be disabled by the process. Not settable: see below. |
| `cfib` | **Backward-Edge Control Flow Integrity.** The hardware shadow stack, Intel CET, is locked on and cannot be disabled by the process. |
| `pie` | **Position-Independent Executable Requirement.** Non-PIE binaries are rejected at exec, so that ASLR is actually effective. |
| `sml` | **Speculation Mitigation Lock.** Speculation mitigations are locked on and cannot be disabled by the process. |

Mitigations are inherited from the parent at fork and can be set by
syscall, typically by peinit between fork and exec. They are one-way:
once set they are never cleared, and exec does not reset them — a
mitigation set by the launcher persists regardless of which binary is
loaded.

Setting a bit is **activation-backed**. Before a mitigation bit moves
from clear to set, KACS either activates the underlying protection for
the target process or verifies that the process already satisfies the
invariant. If any requested mitigation cannot be activated or
verified, the whole operation fails closed without mutating any bit
from that request. Once committed, later operations that would disable
the protection or make the process violate the invariant are rejected,
and re-requesting an already-set mitigation never weakens what is
already committed.

For the runtime memory mitigations, activation covers existing state
as well as future transitions. Enabling `wxp` fails if the process
already has a mapping that is simultaneously writable and executable,
or otherwise already violates the invariant in a way KACS can observe.
Enabling `tlp` fails if the process already has a file-backed
executable mapping whose kernel-resolved path is missing,
unresolvable, outside the approved prefix cache, or otherwise
TLP-denied. Enabling `lsv` fails if the process already has a
file-backed executable mapping whose signing material is missing,
invalid, or below the required PIP trust. Anonymous executable
mappings are governed by `wxp` alone; `tlp` and `lsv` apply only to
file-backed ones.

For the architecture-backed mitigations, activation goes through the
architecture's kernel interface to place the process in the protected
state and prevent later process-controlled disablement. Enabling
`cfif`, `cfib`, or `sml` fails closed when the platform cannot make
the protection true for the target.

`sml` also accepts a second route: a platform that reports speculation
as unconditionally not-affected satisfies activation by that fact
alone, with nothing to enable.

`cfif` cannot currently be committed at all. Activation against a live
task returns `ENODEV` unconditionally, because the kernel exposes no
userspace control surface for IBT or BTI, so the bit fails closed on
every request. `cfib` works, through shadow-stack enable-and-lock,
with one restriction: enabling it on a task other than the caller
fails.

Two mitigations are event-gated rather than retroactive: `pie` is
enforced at subsequent exec and `no_child_process` at subsequent
process creation. They still follow the one-way commit rule, and have
to be set before the event they are meant to constrain.

All of this is distinct from PIP. Mitigations are policy set by the
launcher; PIP is a property of the binary determined by the kernel.

The mitigations compose deliberately: LSV ensures only signed
libraries load, WXP blocks code injection, CFIF blocks forward-edge
code reuse through indirect calls and jumps, CFIB blocks
return-oriented programming, and PIE makes ASLR effective. Together
they make exploitation dramatically harder than any one of them alone.

## 3.3.2.4 UI access (one-way)

| Field | Description |
|---|---|
| `ui_access` | Permits interaction with higher-integrity UI elements. Reserved for future desktop functionality. Set by syscall, typically by peinit between fork and exec, and fixed thereafter. |

## 3.3.2.5 Process restrictions (one-way)

| Field | Description |
|---|---|
| `no_child_process` | Once set, the process creates no child processes — fork, or clone without `CLONE_THREAD`. New threads are unaffected, and the flag is never cleared. |

Unlike the exec-time fields, this one can be set at two points. The
parent's code, running in the freshly forked child, can set it before
exec, so the new binary loads with the restriction already in place.
Or a process can restrict itself at any time during its life — after
it has finished spawning its own workers, for instance.

## 3.3.2.6 The TLP cache

The approved directory prefixes for TLP live in a global kernel cache
rather than on individual PSBs: the `tlp` flag decides whether a
process is subject to enforcement, while the paths themselves are
machine-wide.

The cache is an array of absolute directory prefix byte strings
evaluated against kernel-resolved Linux path bytes, holding at most 64
entries of at most 4096 bytes each. Every prefix begins with `/` and
**ends** with `/` — so that `/usr/lib/` does not match `/usr/libevil`
— and contains no embedded NUL byte. An empty, relative,
NUL-containing, or non-slash-terminated prefix is invalid and is
rejected without mutating the existing cache, which is staged and
swapped under a mutex so a rejected update cannot leave it partly
written.

**The cache has no production writer.** The only code that populates
it is a test helper compiled in solely under the KUnit configuration:
there is no syscall, no securityfs node, and no registry path that
fills it. In a shipping build the cache is therefore permanently
empty — and since an empty cache matches no path, enabling `tlp` on a
process denies *every* file-backed executable mapping it subsequently
attempts.

At `mmap(PROT_EXEC)` time, a process with TLP enabled has the mapped
file's current kernel-resolved backing path checked against every
approved prefix. If the path cannot be resolved, if no prefix matches,
or if the cache is empty, the mapping is rejected.

## 3.3.2.7 Identity virtualization (reserved)

| Field | Description |
|---|---|
| `virtualization` | Per-process state for setuid compatibility redirection. Not active, and implementations may omit the field until it is. |

---

# 3.3.3 Process Security Descriptors

_Peios / Advanced Peios / PKM / KACS / The Process Security Block_

> The descriptor controlling who may operate on a process — the access rights, signal classification, what bypasses the check, and the default.

Every process carries a security descriptor controlling who may
operate on it, stored on the PSB alongside the PIP and mitigation
fields. It replaces Linux's UID-based process access control — a
patchwork of UID comparisons and capabilities — with a single
descriptor evaluation.

## 3.3.3.1 Process access rights

| Right | Value | Meaning |
|---|---|---|
| `PROCESS_TERMINATE` | 0x0001 | Send signals whose default action is termination. |
| `PROCESS_SIGNAL` | 0x0002 | Send informational signals whose default action is to ignore: `SIGCHLD`, `SIGURG`, `SIGWINCH`. |
| `PROCESS_VM_READ` | 0x0010 | Read process memory — ptrace peek, `/proc/<pid>/mem`, `process_vm_readv`. |
| `PROCESS_VM_WRITE` | 0x0020 | Write process memory — ptrace poke, `/proc/<pid>/mem`, `process_vm_writev`. Includes debugger attach. |
| `PROCESS_DUP_HANDLE` | 0x0040 | Extract file descriptors from the process through `pidfd_getfd`. |
| `PROCESS_SET_INFORMATION` | 0x0200 | Change priority, CPU affinity, I/O priority, resource limits, process group membership where Linux permits it, timer slack, memory-placement policy or pages, and mutable `/proc/<pid>` task state — `sched`, `autogroup`, `timens_offsets`, `timerslack_ns`, `coredump_filter`, `oom_adj`, `oom_score_adj`, `make-it-fail`, `fail-nth`, `latency` and `clear_refs`, plus write intent on the coupled `uid_map`, `gid_map`, `projid_map` and `setgroups` seq files. |
| `PROCESS_QUERY_INFORMATION` | 0x0400 | Inspect the process's token; read the detailed `/proc/<pid>/*` files — `cmdline`, `status`, `io`, `limits`, `sched`, `autogroup`, `timens_offsets`, `personality`, `syscall`, `latency`, `timers`, `timerslack_ns`, `mounts`, `mountinfo`, `mountstats`, `coredump_filter`, `oom_adj`, `oom_score_adj`, `loginuid`, `make-it-fail`, `fail-nth`, `seccomp_cache`, `ksm_merging_pages` and `ksm_stat` — plus read intent on the coupled `uid_map`, `gid_map`, `projid_map` and `setgroups` seq files; query Linux compatibility capability state through `capget(pid)`; and query detailed scheduler, CPU-affinity and I/O-priority state. |
| `PROCESS_SUSPEND_RESUME` | 0x0800 | Send signals whose default action is to stop or continue. |
| `PROCESS_QUERY_LIMITED` | 0x1000 | Read basic process information: PID, process group ID, session ID, image name, state, CPU and memory usage — `stat`, `statm`, `comm`, `wchan`, `schedstat`, `cpuset`, `cgroup`, `cpu_resctrl_groups`, `oom_score`, `sessionid`, `patch_state`, `stack_depth` and `arch_status`. This is what `ps` and `top` show, it covers `/proc/<pid>/stat`, and it is the right required for `pidfd_open()` and for `kill(pid, 0)` existence probes. |
| `READ_CONTROL` | 0x20000 | Read the process's own descriptor. |
| `WRITE_DAC` | 0x40000 | Modify the process's DACL. |
| `WRITE_OWNER` | 0x80000 | Change the descriptor's owner. |

Three `/proc` entries are not where the right names suggest.
`maps`, `fd` and `environ` are not gated by
`PROCESS_QUERY_INFORMATION`: they keep their upstream
`PTRACE_MODE_READ_FSCREDS` gating, which maps to **`PROCESS_VM_READ`**
— reading a process's memory map is treated as reading its memory,
which is defensible but is not what the right's name implies. And
`cgroup` sits in the **`PROCESS_QUERY_LIMITED`** set rather than the
detailed one.

## 3.3.3.2 Signal classification

Each Linux signal maps to a process access right according to its
default action.

Signal 0 is not delivered at all. A `kill()`, `tkill()`, or `tgkill()`
call with signal 0 is an existence and permission probe, requiring
`PROCESS_QUERY_LIMITED` on the target plus PIP dominance.

**`PROCESS_TERMINATE`** — default action terminate, or terminate with
a core dump:

| Signal | # | Default | Notes |
|---|---|---|---|
| `SIGHUP` | 1 | Terminate | Session hangup |
| `SIGINT` | 2 | Terminate | Ctrl-C |
| `SIGQUIT` | 3 | Terminate + core | Quit request |
| `SIGILL` | 4 | Terminate + core | Illegal instruction |
| `SIGTRAP` | 5 | Terminate + core | Debug trap |
| `SIGABRT` | 6 | Terminate + core | Abort |
| `SIGBUS` | 7 | Terminate + core | Bus error |
| `SIGFPE` | 8 | Terminate + core | Floating point exception |
| `SIGKILL` | 9 | Terminate | Forced kill, cannot be caught |
| `SIGUSR1` | 10 | Terminate | User-defined |
| `SIGSEGV` | 11 | Terminate + core | Segfault |
| `SIGUSR2` | 12 | Terminate | User-defined |
| `SIGPIPE` | 13 | Terminate | Broken pipe |
| `SIGALRM` | 14 | Terminate | Alarm timer |
| `SIGTERM` | 15 | Terminate | Graceful termination request |
| `SIGSTKFLT` | 16 | Terminate | Stack fault |
| `SIGXCPU` | 24 | Terminate + core | CPU time exceeded |
| `SIGXFSZ` | 25 | Terminate + core | File size exceeded |
| `SIGVTALRM` | 26 | Terminate | Virtual timer |
| `SIGPROF` | 27 | Terminate | Profiling timer |
| `SIGIO` | 29 | Terminate | I/O possible |
| `SIGPWR` | 30 | Terminate | Power failure |
| `SIGSYS` | 31 | Terminate + core | Bad syscall |

**`PROCESS_SUSPEND_RESUME`** — default action stop or continue:

| Signal | # | Default | Notes |
|---|---|---|---|
| `SIGSTOP` | 19 | Stop | Forced stop, cannot be caught |
| `SIGTSTP` | 20 | Stop | Terminal stop, Ctrl-Z |
| `SIGTTIN` | 21 | Stop | Background read from terminal |
| `SIGTTOU` | 22 | Stop | Background write to terminal |
| `SIGCONT` | 18 | Continue | Resume a stopped process |

**`PROCESS_SIGNAL`** — default action ignore:

| Signal | # | Default | Notes |
|---|---|---|---|
| `SIGCHLD` | 17 | Ignore | Child status change |
| `SIGURG` | 23 | Ignore | Urgent socket data |
| `SIGWINCH` | 28 | Ignore | Window resize |

The real-time signals, `SIGRTMIN` through `SIGRTMAX` (32–64), default
to terminate and therefore require `PROCESS_TERMINATE`.

### 3.3.3.2.1 What bypasses the check

This classification applies only to signals sent by userspace through
`kill()`, `tkill()`, and `tgkill()`. Kernel-generated signals —
hardware faults such as `SIGSEGV`, `SIGBUS` and `SIGFPE`, `SIGCHLD`
from a child exiting, `SIGPIPE` from a broken pipe — are delivered by
the kernel and bypass the process descriptor check entirely, because
the `task_kill` LSM hook does not fire for kernel-originated delivery.

Terminal-generated job control signals are kernel-originated under
that rule and bypass the check the same way: `SIGINT`, `SIGQUIT` and
`SIGTSTP` from the tty driver's `isig` handling, and `SIGHUP` on
hangup. This is intentional. Authorization for keyboard-driven signals
is possession of the controlling terminal, which was gated by the
terminal's file descriptor at open time — so Ctrl-C reaches the whole
foreground process group even when a member of it is more privileged
or more PIP-trusted than whoever holds the terminal. A process that
cannot accept that exposure must not attach to an untrusted
controlling terminal.

The `si_uid` in a delivered signal's `siginfo_t` is the sender's
projected UID (§3.10), captured at send time. Like every projected
credential surface it is informational only and is not an
authorization input; `si_pid` carries the same caveat and is subject
to PID reuse besides.

## 3.3.3.3 Generic mapping

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `PROCESS_QUERY_INFORMATION \| PROCESS_VM_READ \| READ_CONTROL` |
| `GENERIC_WRITE` | `PROCESS_SET_INFORMATION \| PROCESS_VM_WRITE \| WRITE_DAC` |
| `GENERIC_EXECUTE` | `PROCESS_TERMINATE \| PROCESS_SUSPEND_RESUME \| PROCESS_QUERY_LIMITED` |
| `GENERIC_ALL` | every process right above, together with `READ_CONTROL`, `WRITE_DAC` and `WRITE_OWNER` |

## 3.3.3.4 The default process descriptor

Every process receives a default descriptor at creation:

```
Owner: <creator's primary token user SID>
Group: <creator's primary token primary group SID>
DACL:
  ALLOW  <process's own user SID>   GENERIC_ALL
  ALLOW  BUILTIN\Administrators     GENERIC_ALL
  ALLOW  SYSTEM                     GENERIC_ALL
  ALLOW  Everyone                   PROCESS_QUERY_LIMITED
```

A process can therefore do anything to itself; Administrators and
SYSTEM have full control over every process; everyone can see basic
process information, which is what makes `ps` and `top` work for all
users; and detailed inspection — token, memory, environment — is
restricted to the process itself, administrators, and SYSTEM.

A service can modify its own descriptor at runtime with
`kacs_set_sd`, which requires `WRITE_DAC` — granted to the process
itself by the default DACL. Requesting a custom descriptor *at launch*
through a service definition is not implemented: the only descriptor
creation path always builds the default template, so every process
starts from it and any deviation is a subsequent write.

## 3.3.3.5 How PIP relates to it

PIP and the process descriptor are complementary, and both checks have
to pass. The descriptor controls *who* may operate on the process; PIP
controls *what trust level* is required for invasive access to a
protected one. AccessCheck evaluates the caller's token against the
target's descriptor for the requested right, and PIP evaluates the
caller's trust against the target's `pip_type` and `pip_trust` for
operations crossing the process boundary.

The two are genuinely independent. A process may have a permissive
descriptor granting Administrators `GENERIC_ALL` and still be
PIP-protected, so administrators pass the descriptor check and are
stopped only by insufficient PIP trust.

The converse — a process with no PIP protection carrying a restrictive
descriptor that denies even administrators — holds with one
qualification. When a descriptor check denies access and PIP was not
the deciding factor, an enabled `SeDebugPrivilege` on the caller
grants the access anyway and is marked used. The privilege therefore
rescues a descriptor denial while remaining unable to cross a PIP
boundary, which is exactly the split §3.4.2 describes for it.

---

# 3.3.4 PSB Lifecycle

_Peios / Advanced Peios / PKM / KACS / The Process Security Block_

> What the PSB does across fork, exec and CLONE_THREAD, and how it feeds AccessCheck.

## 3.3.4.1 Fork

The child receives a copy of the parent's PSB with a single exception:
`process_guid` is not copied, and the child is given a new
kernel-generated one. Everything else — the PIP fields, the
mitigations, and any active restrictions — is inherited, so a
Protected process's children start Protected and PIP propagates across
fork.

The child also receives a new default process descriptor. Its owner is
the forking thread's **primary** token's user SID, not the
impersonation token's, even when the thread is impersonating at the
time. The DACL follows the default template.

## 3.3.4.2 Exec

The PIP fields are reset at exec from the new binary's cryptographic
signature. A Protected parent that execs an unsigned binary loses PIP
protection: protection follows the binary, not the lineage.

The mitigation flags — `lsv`, `wxp`, `tlp`, `cfif`, `cfib`, `pie`,
`sml`, `ui_access` — are not reset. They persist across exec
unchanged, so a mitigation set between fork and exec survives whatever
binary is subsequently loaded. `no_child_process` persists in the same
way: a process restricted from creating children stays restricted no
matter what it execs.

`process_guid` is not reset either, because it identifies the process
— the scheduling entity — rather than the binary.

The process descriptor is not reset. Exec preserves it unchanged. It
was initialised at fork from the forking thread's primary token and
reflects the process creation context, or a later explicit management
context, rather than the binary being executed. Primary token
installation and the other explicit process-descriptor mutation paths
replace or modify it only under their own rules (§3.2.3).

## 3.3.4.3 Clone with CLONE_THREAD

Threads share the process's PSB. Thread creation is unaffected by
`no_child_process`, which blocks new processes only.

## 3.3.4.4 Relationship to AccessCheck

The PSB is not an input to AccessCheck in the general case. AccessCheck
takes a token and a descriptor and evaluates access; most PSB fields
are invisible to it.

PIP is the exception. The pipeline includes a PIP enforcement step
reading `pip_type` and `pip_trust`, which come from the PSB rather
than from any token: the enforcement layer extracts them and passes
them to AccessCheck as explicit parameters.

The asymmetry between MIC and PIP follows from that. **MIC** uses the
effective token, so impersonation changes how it evaluates — which is
safe because the integrity ceiling on impersonation (§3.5.2) prevents
escalation. **PIP** uses the PSB, so impersonation cannot change how
it evaluates — necessary because there is no impersonation gate
constraining the PIP dimensions, making the PSB the only safe source.

The process mitigations and `no_child_process` do not interact with
AccessCheck at all. Each is enforced at its own enforcement point,
independently of the access control pipeline.

---

# 3.4.1 The Privilege Model

_Peios / Advanced Peios / PKM / KACS / Privileges_

> Rights that do not fit the subject-object model — the privilege lifecycle, the two enforcement categories, intent gating, assignment and auditing.

Some operations do not fit the subject-object model at all. Rebooting
the machine, loading a kernel module, changing the system clock,
creating a token — these affect the system itself rather than a
specific protected resource, so there is nothing to attach a security
descriptor to. They still need authorization.

Privileges fill that gap. A privilege is the right to perform a
particular system operation, carried on the token beside the
principal's identity. Where a descriptor says "this principal may read
this file", a privilege says "this principal may shut down the
system". The descriptor lives on the object; the privilege lives on
the subject.

## 3.4.1.1 Lifecycle

A privilege is **assigned by policy** when authd creates the token,
resolving the principal's assignments from security policy once, at
creation. There are no runtime grants: a privilege absent at creation
can never be added later.

The privilege then sits on the token in whatever enabled state it was
created with. The kernel accepts any enabled set that is a subset of
the present set, and takes the creation-time enabled set as the
enabled-by-default set. authd issues every privilege it grants already
enabled, on the reasoning that a privilege the holder had to enable
before it worked would be a grant in name only — so the
present-but-disabled resting state exists in the model and is
reachable through AdjustPrivileges, but is not where privileges
normally start.

A privilege that has been disabled is re-activated by **explicitly
enabling** it through AdjustPrivileges.

When the privilege is **exercised**, the kernel checks that it is both
present and enabled — a single mask test against both words — permits
the operation, and records it as used.

For a standalone gate, "exercised" means the gate accepted that bit,
and the used bit is meant to be recorded even when a later independent
check — a process descriptor, PIP, or a malformed-input test — denies
the operation afterwards. Where the shared privilege helper performs
the check, it marks the bit immediately, and the impersonation gate
does the same. Three gates mark later and therefore record nothing
when a subsequent check fails: token creation marks after the token
has been constructed and its descriptor allocated, so a malformed
specification leaves the bit unset; primary token installation marks
after the same-user and same-LogonSession gate; and the `CAP_SYS_BOOT`
mapping marks after the remote-shutdown origin gate.

Used-state for the AccessCheck-influencing privileges follows the
provenance rules of §3.8 instead.

Recording the used bit is not merely bookkeeping. Every gate treats a
failure to record it as a failure of the operation itself and returns
`EPERM` or `EACCES`.

Afterwards the privilege may be **disabled**, returning to rest, or
**removed permanently**, which clears it from the present, enabled,
and enabled-by-default states while preserving the `used` bit for
audit.

## 3.4.1.2 Two enforcement categories

**Standalone operation gates** are the majority. They authorize
specific operations that AccessCheck does not mediate — rebooting,
loading modules, debugging processes — and the kernel simply checks
whether the calling thread's token holds the privilege present and
enabled before allowing the operation.

**AccessCheck-influencing privileges** alter the outcome of
AccessCheck itself, causing it to grant rights the object's DACL would
not grant on its own. They are evaluated inside the pipeline alongside
DACL rules, integrity policy, and confinement. There are five:
`SeSecurityPrivilege` grants `ACCESS_SYSTEM_SECURITY` for SACL access
(and doubles as a standalone gate for the audit-related Linux
capabilities); `SeTakeOwnershipPrivilege` grants `WRITE_OWNER` as a
post-DACL fallback; `SeBackupPrivilege` grants all read access;
`SeRestorePrivilege` grants all write access plus `WRITE_DAC`,
`WRITE_OWNER`, `DELETE` and `ACCESS_SYSTEM_SECURITY`; and
`SeRelabelPrivilege` loosens MIC's constraint on `WRITE_OWNER` for
non-dominant callers. §3.8 gives the exact mechanics.

## 3.4.1.3 Intent gating

`SeBackupPrivilege` and `SeRestorePrivilege` are intent-gated. Other
AccessCheck-influencing privileges are self-scoping —
`SeSecurityPrivilege` only matters when `ACCESS_SYSTEM_SECURITY` is
requested — but backup and restore grant such broad categories of
access that evaluating them unconditionally would apply them to every
AccessCheck on the system.

AccessCheck therefore takes a `privilege_intent` parameter. A caller
passes `BACKUP_INTENT` for a backup-context operation and
`RESTORE_INTENT` for a restore-context one, and the corresponding
privilege is evaluated only when its flag is present. Without the
flag, these privileges are invisible to the pipeline.

Intent gating also keeps backup and restore *inside* the pipeline
rather than short-circuiting it, which matters because later stages —
PIP in particular — have to be able to constrain privilege-granted
access.

## 3.4.1.4 Assignment

Privileges are assigned by security policy, not by identity.
Membership of the Administrators group confers no privilege by itself.
Groups and privileges are orthogonal: groups determine which objects
you can reach through DACLs, privileges determine which system
operations you can perform.

An administrator defines policy — that members of Backup Operators
receive `SeBackupPrivilege` and `SeRestorePrivilege`, say. At
authentication authd resolves the principal's group memberships,
evaluates the policy against them, and creates the token carrying the
result. The kernel neither verifies nor evaluates that policy; it
trusts authd as a TCB component. The token then carries those
privileges for its whole lifetime.

## 3.4.1.5 Auditing

Every exercise sets the token's monotonic used state for that
privilege, and every standalone gate emits an ftrace event.

KMES audit events are emitted only for the five AccessCheck-influencing
privileges, and only when the token's `audit_policy` opts in through
`PRIVILEGE_USE_SUCCESS` or `PRIVILEGE_USE_FAILURE`. The event fires
when the privilege's provenance bits intersect both the mapped desired
mask and the final granted mask. For `SeSecurityPrivilege` and
`SeTakeOwnershipPrivilege` that intersection is genuinely
counterfactual — `ACCESS_SYSTEM_SECURITY` is pre-decided by the
privilege, and take-ownership contributes only when `WRITE_OWNER` was
not already granted — so an event means the privilege was load-bearing.
Backup and restore seed their bits unconditionally, without asking
whether the DACL would have granted the same access, so their events
also fire for accesses the DACL alone would have permitted.

A `MAXIMUM_ALLOWED` request short-circuits this accounting entirely,
recording no used bits and emitting no privilege-use events for any
privilege.

---

# 3.4.2 Privilege Catalogue

_Peios / Advanced Peios / PKM / KACS / Privileges_

> Every Peios privilege with the bit it occupies in the token's four privilege words, grouped by what it governs.

The complete set of Peios privileges, with the bit each occupies in
the token's four 64-bit privilege words. Format-compatible privileges
sit at their standard Windows LUID positions in bits 2–35; custom
Peios privileges are allocated downward from bit 63, so that a
privilege defined by a future AD release cannot collide with one of
ours.

Enforcement classes are: **kernel standalone**, enforced at a specific
operation boundary independently of AccessCheck; **AccessCheck**,
evaluated inside the pipeline; **AccessCheck + standalone**, both;
**application-level**, checked by a userspace service rather than the
kernel; and **reserved**, allocated for format compatibility with no
enforcement point.

## 3.4.2.1 Identity and token management

| Privilege | Bit | Mask | Enforcement |
|---|---:|---|---|
| `SeCreateTokenPrivilege` | 2 | 0x4 | Kernel standalone |
| `SeAssignPrimaryTokenPrivilege` | 3 | 0x8 | Kernel standalone |
| `SeImpersonatePrivilege` | 29 | 0x20000000 | Kernel standalone |

`SeCreateTokenPrivilege` mints tokens from scratch, and only TCB
components — authd and peinit — carry it. `SeImpersonatePrivilege`
lets a service impersonate a principal other than itself, and every
service that handles requests on behalf of users needs it; it is
checked in exactly one place, the impersonation identity gate (§3.5.2).

`SeAssignPrimaryTokenPrivilege` gates installing a token as a
process's primary identity. Installation is **self-directed**:
`KACS_IOC_INSTALL` acts on the calling process and fans out to the
sibling threads of its own thread group. There is no mechanism for
installing a token on a different process (§3.2.3). The kernel also
requires the new token to carry the same user SID and the same
LogonSession as the outgoing one, unless the caller additionally holds
`SeTcbPrivilege`. The privilege is also consulted as a *deny* gate on
the exec and credential projection paths.

## 3.4.2.2 Access control

| Privilege | Bit | Mask | Enforcement |
|---|---:|---|---|
| `SeSecurityPrivilege` | 8 | 0x100 | AccessCheck + standalone |
| `SeTakeOwnershipPrivilege` | 9 | 0x200 | AccessCheck |
| `SeBackupPrivilege` | 17 | 0x20000 | AccessCheck + standalone |
| `SeRestorePrivilege` | 18 | 0x40000 | AccessCheck + standalone |
| `SeRelabelPrivilege` | 32 | 0x1_0000_0000 | AccessCheck + standalone |
| `SeChangeNotifyPrivilege` | 23 | 0x800000 | Kernel standalone |
| `SeCreateSymbolicLinkPrivilege` | 35 | 0x8_0000_0000 | Kernel standalone |

`SeSecurityPrivilege` reads and writes an object's SACL, and also
gates `CAP_AUDIT_CONTROL`, `CAP_MAC_ADMIN` and `CAP_AUDIT_READ`
through the capability mapping, the KMES ring buffer attach, and
supplying a SACL at object creation.

`SeTakeOwnershipPrivilege` takes ownership of any object regardless of
its permissions. It is the only privilege here with no standalone
enforcement point at all — it exists purely inside AccessCheck.

`SeBackupPrivilege` and `SeRestorePrivilege` read and write any object
regardless of the DACL. Inside AccessCheck they are intent-gated
(§3.4.1), but both are also used as plain standalone gates outside it:
restore on the descriptor replacement path when the cache is invalid
and on owner assignment to a SID the subject does not hold, and both
on the registry key backup and restore paths.

`SeRelabelPrivilege` changes an object's integrity label, punching
`WRITE_OWNER` through MIC for non-dominant callers and removing the
at-or-below-own-level restriction when a label is written.

`SeChangeNotifyPrivilege` bypasses traverse checking — without it,
reaching a file requires `FILE_TRAVERSE` on every intermediate
directory. The bypass has one exception the name does not suggest: it
is suppressed when the access carries `MAY_CHDIR`, so an explicit
`chdir` takes a full `FILE_TRAVERSE` check whether or not the caller
holds the privilege. The privilege additionally gates
`open_by_handle_at`, which has nothing to do with traversal. It is
checked once per intermediate directory on every path resolution, and
each check takes the token's mutation lock for a snapshot and then
performs a used-bit update, so the cost is O(depth) locked operations
per path walk.

`SeCreateSymbolicLinkPrivilege` creates symbolic links, and is
required in addition to `FILE_ADD_FILE` on the parent directory.

## 3.4.2.3 System operations

| Privilege | Bit | Mask | Enforcement |
|---|---:|---|---|
| `SeTcbPrivilege` | 7 | 0x80 | Kernel standalone |
| `SeLockMemoryPrivilege` | 4 | 0x10 | Kernel standalone |
| `SeIncreaseQuotaPrivilege` | 5 | 0x20 | Kernel standalone |
| `SeLoadDriverPrivilege` | 10 | 0x400 | Kernel standalone |
| `SeSystemProfilePrivilege` | 11 | 0x800 | Kernel standalone |
| `SeSystemtimePrivilege` | 12 | 0x1000 | Kernel standalone |
| `SeProfileSingleProcessPrivilege` | 13 | 0x2000 | Kernel standalone |
| `SeIncreaseBasePriorityPrivilege` | 14 | 0x4000 | Kernel standalone |
| `SeManageVolumePrivilege` | 28 | 0x10000000 | Kernel standalone |
| `SeShutdownPrivilege` | 19 | 0x80000 | Kernel standalone |
| `SeDebugPrivilege` | 20 | 0x100000 | Kernel standalone |
| `SeAuditPrivilege` | 21 | 0x200000 | Kernel standalone |
| `SeRemoteShutdownPrivilege` | 24 | 0x1000000 | Kernel standalone |

`SeTcbPrivilege` is the catch-all for system operations with no more
specific privilege, and only TCB services need it. It has by far the
widest reach of any privilege: fourteen Linux capability mappings,
several token operations, the mount policy paths, the central access
policy cache, LogonSession creation and destruction, removal of
mandatory resource attribute ACEs during descriptor merge, a KMES rate
limit exemption, the LCS source authentication path, and the upgrade
of a linked-token query from an Identification-level copy to the real
token at full access.

`SeShutdownPrivilege` shuts down or reboots the machine, mapped
through `CAP_SYS_BOOT`. `SeRemoteShutdownPrivilege` is required *in
addition* when the request originates from a Network,
NetworkCleartext, or NewCredentials logon.

`SeLoadDriverPrivilege` loads and unloads kernel modules through
`CAP_SYS_MODULE`. `SeDebugPrivilege` attaches to and inspects any
process regardless of its descriptor, and does not bypass PIP (§3.7).
`SeSystemtimePrivilege` changes the clock,
`SeIncreaseBasePriorityPrivilege` raises scheduling priority and sets
CPU affinity for other processes, `SeIncreaseQuotaPrivilege` overrides
resource limits, `SeLockMemoryPrivilege` locks pages in physical
memory, and `SeAuditPrivilege` writes events to the audit log — it is
what KMES requires for userspace event emission.

`SeProfileSingleProcessPrivilege` attaches `perf_event_open()` to a
specific other process. It respects PIP dominance, and own-task
profiling requires nothing. `SeSystemProfilePrivilege` covers
system-wide profiling — per-CPU events, all-task sampling, kernel-mode
events — and does not respect PIP at the per-sample level, since
system-wide samples include PIP-protected tasks. It is an
operator-class privilege.

Those two and `SeLoadDriverPrivilege` share one mapping: `CAP_PERFMON`
is satisfied by **any** of the three, and every one the caller holds is
marked used. The two profiling privileges are otherwise disjoint
tiers.

## 3.4.2.4 Network

| Privilege | Bit | Mask | Enforcement |
|---|---:|---|---|
| `SeBindPrivilegedPortPrivilege` | 63 | 0x8000_0000_0000_0000 | Kernel standalone |

Binds TCP and UDP ports below 1024, mapped through
`CAP_NET_BIND_SERVICE`. A custom Peios privilege, retaining the Linux
convention as defence in depth.

## 3.4.2.5 Directory and domain operations

| Privilege | Bit | Enforcement |
|---|---:|---|
| `SeSyncAgentPrivilege` | 26 | Application-level |
| `SeEnableDelegationPrivilege` | 27 | Application-level |
| `SeMachineAccountPrivilege` | 6 | Application-level |

`SeSyncAgentPrivilege` reads every object in the directory regardless
of per-object permissions, for AD replication agents.
`SeEnableDelegationPrivilege` marks a principal as trusted for
delegation. `SeMachineAccountPrivilege` adds computer accounts to the
domain. None has a kernel definition, which is consistent with their
being application-level — but none has a userspace definition in the
tree either, so at present nothing anywhere enforces them.

## 3.4.2.6 Reserved

Allocated for format compatibility so that tokens from Active
Directory environments carry them without information loss. None is
defined in the kernel and none has an enforcement point.

| Privilege | Bit | Reservation rationale |
|---|---:|---|
| `SeCreatePagefilePrivilege` | 15 | Absorbed into `SeTcbPrivilege`. |
| `SeCreatePermanentPrivilege` | 16 | No Linux equivalent. |
| `SeSystemEnvironmentPrivilege` | 22 | Gated by descriptors on efivar files under FACS. |
| `SeUndockPrivilege` | 25 | Server operating system. |
| `SeCreateGlobalPrivilege` | 30 | Peios has no per-LogonSession object namespaces. |
| `SeTrustedCredManAccessPrivilege` | 31 | Reserved for future secrets infrastructure. |
| `SeIncreaseWorkingSetPrivilege` | 33 | Linux does not gate memory residency hints. |
| `SeTimeZonePrivilege` | 34 | Linux does not gate timezone changes. |

## 3.4.2.7 Unallocated and unnamed bits

`SeCreateJobPrivilege` is allocated bit 62 for submitting supervised
jobs through JFS. No kernel definition exists for it. The bit is
nevertheless included in the boot SYSTEM token's privilege set, which
covers bits 2–35 together with 62 and 63, so the SYSTEM token holds
bit 62 present and enabled with no name attached to it and no gate
that consults it.

Nothing validates a privilege mask against the allocated set. Token
creation checks only that the enabled set is a subset of the present
set, and adjustment accepts any bit index from 0 to 63. Bits 0, 1, and
36–61 — positions the catalogue does not allocate at all — can
therefore be set at creation and disabled or removed afterwards
without error, and are simply inert.

## 3.4.2.8 Default grants

`SeChangeNotifyPrivilege` is granted to every principal, as an authd
policy decision rather than a kernel one: the issuer's floor grants it
to Everyone. `SeCreateSymbolicLinkPrivilege` is **not** granted by
default despite being the other traditional default-grant privilege —
authd deliberately omits it from the floor, and no shipped seed grants
it. Either can be removed from a specific token by FilterToken.

---

# 3.5.1 Impersonation Levels

_Peios / Advanced Peios / PKM / KACS / Impersonation_

> The four levels at which a server thread may assume a client's identity, and what each permits.

Impersonation lets a server thread temporarily assume a client's
identity, so that access control decisions on that thread evaluate the
client's token instead of the server's.

The client controls how far its identity can travel by setting an
impersonation level on the connection before it is established, and
the server cannot escalate beyond the level the client chose. There is
no API that bypasses that choice.

**Anonymous.** The server cannot identify the caller at all. The
connection carries no identity information: both token inspection and
impersonation yield a token whose user SID is Anonymous (`S-1-5-7`),
which carries Everyone as an enabled group and does not carry
Authenticated Users.

**Identification.** The server can identify the caller — read SIDs,
query groups, inspect privileges — but cannot act as them. An
Identification-level token is barred from AccessCheck against
resources: a server thread impersonating one and attempting to open a
file simply fails the check.

**Impersonation.** The server can act as the caller for all local
operations, including ones that cross local IPC boundaries. If service
A impersonates client B at this level and connects to local service C,
C sees B's identity — identity cascades freely across local services.
This is the default.

**Delegation.** Locally identical to Impersonation. The distinction
activates at the network boundary, where a Delegation-level token
carries authorization for the server to forward the client's identity
to services on other machines through Kerberos. KACS enforces the
level; authd is what acts on it.

The level is set by the client through a KACS syscall on the socket
before `connect()`, and defaults to Impersonation.

---

# 3.5.2 Impersonation Gates

_Peios / Advanced Peios / PKM / KACS / Impersonation_

> The two independent checks that decide whether an impersonation proceeds at the level asked for — the identity gate and the integrity ceiling.

When a server thread attempts to impersonate a client's token, two
independent checks decide whether it proceeds at the requested level.
Both have to pass. If either fails the effective level is reduced to
Identification — the movement is only ever downward.

Both gates are evaluated against the server's **primary token**
(`real_cred`), never its effective token. A server already
impersonating another client has its gates judged against its own
service identity, so a previous impersonation cannot influence the
next one.

## 3.5.2.1 The identity gate

The identity gate asks whether this server may impersonate this
particular user's identity. Impersonation at Impersonation or
Delegation level is permitted if either of two conditions holds.

**Same user, same restriction status** — the server's primary token
and the client's token carry the same user SID, and both are
restricted or both unrestricted.

**`SeImpersonatePrivilege`** — the server's primary token holds it,
enabled.

If neither holds, the level is **silently capped to Identification**.
No error is returned: the call succeeds, and the resulting token is
merely at Identification level.

There is one hard denial. A **restricted** server impersonating an
**unrestricted** client of the same user is rejected outright with
`-EPERM` rather than capped, because that is precisely how a sandboxed
process would escape by impersonating its parent's unrestricted token.
The reverse direction, unrestricted server to restricted client, is a
harmless downgrade and takes the ordinary cap-to-Identification path.

MS-DTYP includes a third condition — an origin LogonSession check
letting the session that created a token impersonate it without the
privilege. KACS drops it. A service needing to impersonate a different
user holds `SeImpersonatePrivilege`, and there are no hidden paths.

## 3.5.2.2 The integrity ceiling

The integrity ceiling asks whether the client's token sits at an
integrity level the server is allowed to assume. To act at
Impersonation or Delegation level, the client token's integrity level
has to be less than or equal to the server primary token's. A
Medium-integrity server can impersonate Low or Medium clients; against
a High-integrity client the level caps to Identification.

The installed token may keep the client's literal integrity label as
identity metadata after the cap, but that preserved label authorizes
nothing, because Identification-level tokens are barred from
AccessCheck entirely.

The ceiling exists because MIC evaluates the *effective* token's
integrity level for tokens that can act. Without it, a server could
impersonate a higher-integrity token and gain write access to
higher-integrity objects — integrity escalation through impersonation.

The ceiling is enforced unconditionally, regardless of privilege.
`SeImpersonatePrivilege` bypasses the identity gate and never the
ceiling. MS-DTYP allows the privilege to bypass every check including
this one; KACS does not, because `mandatory_policy` is immutable here
(§3.2.2) and MIC is consequently a real boundary. Letting a privilege
punch through would give back exactly what that immutability buys.

## 3.5.2.3 Composition

The two gates are independent, both are evaluated, and the effective
level is the minimum any constraint permits: start from the level the
client set on the socket, cap to Identification if the identity gate
fails, cap to Identification if the integrity ceiling fails, and the
result is the effective impersonation level.

---

# 3.5.3 Impersonation Lifecycle

_Peios / Advanced Peios / PKM / KACS / Impersonation_

> Assuming and reverting an identity — the sequence, Anonymous, double impersonation, and how MIC and PIP interact with it.

## 3.5.3.1 The sequence

**The client connects.** It optionally sets the maximum impersonation
level through a syscall — the default is Impersonation — and calls
`connect()`. The kernel's LSM hook fires on the Unix stream
connection.

**Identity is captured.** The hook examines the client thread's
effective credential together with the socket's maximum level. At
Anonymous, a token whose user SID is `S-1-5-7`, whose enabled groups
include Everyone, and which does not carry Authenticated Users is
stored on the socket's LSM blob, and the client's real identity is
never recorded. At Impersonation or Delegation, the thread's effective
token is stored — and if the connecting thread is itself
impersonating, the impersonated identity is what flows through, which
is how identity cascades across local services. At Identification, the
effective token is stored but tagged at that level.

**The server impersonates** by calling `kacs_impersonate_peer` with
the connection fd. The kernel retrieves the stored token, evaluates
both gates against the server thread's primary token, computes the
effective level, and constructs a new credential carrying the
impersonation token at that level.

**Access control follows the impersonation token.** Every subsequent
AccessCheck on the thread evaluates it, and MIC uses its integrity
level. PIP continues to read the PSB, unchanged.

**The server reverts** with `kacs_revert()`, restoring the thread's
credential to `real_cred` and its service identity with it.

## 3.5.3.2 Anonymous

Any thread may impersonate the Anonymous identity without passing
either gate. Assuming Anonymous is always a downgrade — the token has
no access beyond what is explicitly granted to `S-1-5-7` or Everyone —
so no privilege is needed, no identity gate runs, and no integrity
ceiling applies.

Anonymous tokens carry the Anonymous SID as the user SID, no
privileges, and Untrusted integrity. The socket path constructs that
minimal shape rather than preserving any part of the caller's real
identity.

## 3.5.3.3 Double impersonation

A thread already impersonating that calls `kacs_impersonate_peer`
again causes the kernel to revert internally and then re-impersonate.
Because the gates are evaluated against the primary token, the
previous impersonation has no bearing on the new one.

## 3.5.3.4 Interaction with MIC and PIP

**MIC reads the effective token** for tokens permitted to act, which
is safe precisely because the integrity ceiling makes acting
impersonation safe. A thread cannot act at Impersonation or Delegation
level with a client token whose integrity exceeds the server primary
token's. When the ceiling caps the level to Identification, the
installed token may still preserve the client's literal label as
metadata, but it authorizes no resource access because
Identification-level tokens are barred from AccessCheck. An acting
impersonation token therefore preserves or lowers the server's
integrity, and a higher literal label can only ever exist on a
non-acting Identification-level token.

**PIP reads the PSB**, because there is no equivalent ceiling for it.
PIP operates on `pip_type` and `pip_trust`, which are orthogonal to
integrity level. Reading them from the effective token would let a
process impersonate a token carrying higher PIP values and acquire
protection it has not earned.

## 3.5.3.5 SeImpersonatePrivilege

The privilege permits a service to impersonate arbitrary clients —
those with different user SIDs. Without it a process can impersonate
only tokens matching its own user SID and restriction status.

It is checked against the server's primary token, so a thread already
impersonating one client is judged on its real service identity. It
has to be enabled at the moment of the call. And it bypasses the
identity gate only, never the integrity ceiling.

## 3.5.3.6 Delegation and the network boundary

Locally, Impersonation and Delegation behave identically. The
distinction activates at the network boundary, where a
Delegation-level token carries authorization for Kerberos credential
forwarding to services on other machines.

KACS tracks the level on the token and the socket, and authd checks it
when it needs to perform Kerberos authentication for an impersonating
thread. KACS itself has no Kerberos awareness and no network
awareness: the level is a flag that authd interprets.

## 3.5.3.7 Supported transports

Socket-based impersonation through `kacs_impersonate_peer` works on
two socket types. `SOCK_STREAM` is the connection-oriented byte
stream, and `SOCK_SEQPACKET` is connection-oriented with message
boundaries and uses the same identity capture model. Both follow the
same lifecycle: capture at `connect()`, impersonate, revert.

Three transports do not support it. `SOCK_DGRAM` is connectionless, so
identity would arrive as per-message credentials — a different model
that is not part of this syscall surface; datagram sockets create no
KACS peer token. Sockets from `socketpair()` are pre-connected and
unnamed, and while possession of the fd authorizes use of the channel,
no peer-token snapshot is installed. Pipes and FIFOs have no peer
credential mechanism at all.

For all of these, the universal fallback is explicit token fd
impersonation through `KACS_IOC_IMPERSONATE`, which works regardless
of how the token fd was obtained — socket-based capture, an
`SCM_RIGHTS` transfer, `kacs_open_peer_token`, or any other path.

---

# 3.6 Binary Signature Verification

_Peios / Advanced Peios / PKM / KACS_

> How the kernel verifies signatures on executables and libraries — the key table, finding a signature, hashing, and PIP determination at exec.

Binary signing is the foundation of PIP trust determination and of
Library Signature Verification. The kernel verifies cryptographic
signatures on executable files to establish their trust level, and
signing is the *only* mechanism by which a binary acquires PIP
protection — there is no runtime API that can confer it.

This section describes verification. The signature format itself, and
what a signer has to produce, are specified in PSPK's Binary Signing
and PIP chapter. The kernel only ever verifies; it holds no private
key and contains no signing primitive.

## 3.6.1 The key table

The kernel carries its verification keys in a dedicated data section
of the kernel image, as an array of 1960-byte entries — a raw
1952-byte ML-DSA-65 public key, then a `u32` little-endian PIP type,
then a `u32` little-endian PIP trust — terminated by an all-zero
entry.

Before any cryptographic work, the whole table is walked and
validated. A table with no all-zero terminator is rejected with
`EINVAL`, and so is a table containing any entry whose tier is not
exactly Protected (512) with `PeiosTcb` trust (8192). Both rejections
fail *every* verification on the system, which is what makes the
single-tier constraint absolute rather than conventional: a key at any
other tier does not merely fail to be honoured, it disables signing
entirely.

The consequence is that a verified binary is always Protected/8192.
The Isolated type (1024) is reserved and unreachable, and the
multi-key, multi-tier model the table layout anticipates would need a
change to the validator, not merely an added key.

A build configured for KUnit compiles in a different, hard-coded key
at the same tier, taken from the test vector header. Such a kernel
trusts a publicly known key.

## 3.6.2 Finding a signature

Verification begins by recording the file's current size. Everything
that follows is bounded by that snapshot, and the size is re-read
before the attempt returns — a file that changed size mid-verification
invalidates the whole result.

The first four bytes decide the path. A file matching `\x7fELF` takes
the ELF path; a file shorter than four bytes, or with different magic,
goes straight to the xattr.

On the ELF path the kernel parses the header, locates the section
header table and the section-name string table, and scans sections in
index order for one named exactly `.peios.sig`. The match is over all
eleven bytes including the terminating NUL, so a longer name with that
prefix does not match.

**Finding the section commits the ELF path.** The moment a section
header with that name is found, the xattr is no longer consulted —
whatever happens next. A wrong section type, a size other than 3310, a
range outside the file, an allocation failure, a short read, a bad
version byte, or a hash failure all yield "unsigned" rather than
falling back. This is deliberate: without it, an attacker could craft
a malformed ELF section to force fallback to whichever path they could
more easily control.

Several *structural* ELF failures commit the path too, before any
`.peios.sig` section has been seen: a file shorter than an ELF header,
a class other than `ELFCLASS64`, a byte order other than little-endian,
an unexpected ELF version, a section header entry size other than 64,
an absent or out-of-range section-name string table index, and a
section header table or string table lying outside the recorded size.
A 32-bit or big-endian ELF therefore cannot carry an xattr signature
at all — it is committed to the ELF path and then fails on it. The one
structural case that does *not* commit is `e_shnum == 0`, so an ELF
with no section headers falls through to the xattr normally.

The xattr path reads `security.peios.sig` and requires exactly 3310
bytes; any other size is treated as unsigned. The read bypasses the
LSM xattr hooks, so FACS does not mediate the verifier's own read of
the signature.

## 3.6.3 Hashing and verification

The message signed is a 32-byte SHA-256 content hash, computed
differently depending on where the signature was found. For an ELF
section source, the hash covers the file with the section's *contents*
replaced by zeros — the `Elf64_Shdr` entry describing it is hashed
verbatim, along with everything else. For an xattr source the hash
covers the entire file with no exclusions, and that applies to ELF
files reaching the xattr path as well as to non-ELF ones. Hashing
proceeds in 4 KB chunks, with the zero run emitted in
`SHA256_BLOCK_SIZE` pieces.

The kernel then verifies the 3309-byte signature against each key in
the table, in order, returning on the first success. There is no key
identifier in the blob, so key selection is exhaustive trial and the
cost is one ML-DSA verification per key. The trust tier is a property
of *which key verified*, never of anything the signer encoded.

Verification uses the kernel crypto signature API with the `mldsa65`
algorithm. That API exposes no context parameter, so the empty FIPS
204 context is structurally guaranteed rather than checked — a
signature made under a non-empty context simply fails to verify.

### 3.6.3.1 When verification cannot be performed

"Did not verify" and "could not be verified" are different answers and
are kept apart. The per-key verifier is tri-state: verified, did not
verify, or a negative errno meaning the check could not be made — an
unavailable ML-DSA transform, or a key the transform will not accept.

A negative stops the search rather than trying the remaining keys: the
failure is in the machinery, and every remaining key would meet the same
one. It then propagates out of the exec path, which **refuses the exec**
with `EACCES`.

That asymmetry with the ordinary unsigned path is the point. An unsigned
binary runs with no integrity label, which is legitimate. Treating an
unverifiable one the same way removes PIP from every process the system
executes, and the result is indistinguishable from a correctly working
system that has no signed binaries — so nothing surfaces it until
signing is deployed, at which point it looks like the signing rollout
broke something.

On the LSV path the same condition denies the mapping, which already
fails closed.

### 3.6.3.2 The boot-time probe

A `late_initcall` allocates the transform once and, if it cannot,
emits `pr_err` and a `KACS_SIGNING_CRYPTO_UNAVAILABLE` KMES event
carrying the errno. The condition is then visible at boot rather than
inferred from every process running without an integrity label.

It cannot refuse to start, and two things rule that out rather than one:

- At LSM init the algorithm is not yet registered, so `crypto_alloc_sig`
  returns `ENOENT` on every boot. A probe there would fire always.
- A non-zero return from an LSM's init function is only `WARN`'d
  (`security/lsm_init.c`, `lsm_init_single`). The hooks are never added,
  so "refuse to initialise" means running with no KACS at all — worse
  than the failure it would be preventing.

Enforcement therefore lives at exec, where refusing one exec is
recoverable in a way losing the integrity boundary is not.

The probe is not `IS_ENABLED(CONFIG_CRYPTO_MLDSA)`. That option is an
unconditional `select` under `SECURITY_PKM`, which is a `bool`, so a
config test would always pass and catch nothing. Only calling the
allocator sees the ordering failure.

## 3.6.4 PIP determination at exec

At `execve()` the kernel looks up and verifies the signature as above.
A verified binary takes `pip_type` and `pip_trust` from the matched
key; no signature, an invalid or unstable one, a bad signature, or no
matching key all yield None/0.

**Exec proceeds in every case where the question could be answered.**
PIP is additive protection, not an execution gate: it determines trust
level, not permission to run. The one exception is a signature that
could not be verified at all, described above, which refuses the exec —
because there the question was not answered, so there is no basis on
which to assign a trust level. An
attacker who replaces a signed binary's signature with garbage costs
it PIP protection but can still execute it, subject to FACS. The
asymmetry with LSV — which *does* block unsigned libraries — is
intentional. Exec is permissive; `mmap(PROT_EXEC)` is restrictive.

Determination is transactional with exec success, in three phases. The
pending value is cleared at the top of every `bprm_creds_from_file`
invocation, staged into the task's security blob, and committed to the
process state only from `bprm_committed_creds`. An exec that fails
between staging and commit leaves the process state untouched, and the
stale pending value is cleared by the next exec or at task teardown.

Because the hook fires once per binfmt iteration and each iteration
re-stages, a `#!` script's PIP comes from the **interpreter** — the
last file processed — not from the script. A TCB-signed interpreter
runs at TCB level whatever script it executes. Symlinks need no
special handling either: the LSM hooks receive the already-resolved
target file, so a symlink inherits its target's level by construction.

Once committed, the values are fixed for the lifetime of the process
image, inherited at fork, and re-derived at the child's exec.

## 3.6.5 Content pinning

A binary that verifies to a nonzero tier has its backing inode pinned
as KACS-verified executable content before the exec result is
committed, and LSV pins on success too, before allowing the mapping.
Pinning closes the gap between verifying one byte image and modifying
the same inode afterwards.

If pinning fails at exec, the PIP result is **downgraded to None/0**
rather than the exec being failed or the tier being kept — the process
runs unprotected. Under LSV a pin failure denies the mapping instead.

A pinned inode rejects in-place content mutation even when the size
would be preserved: ordinary, positioned and append writes;
`ftruncate()` and pathname `truncate()`; and every `fallocate` mode,
including allocation-only ones, because the pin check runs before and
independently of the mode-support test. File ioctls that mutate
content, ranges, or allocation are rejected, and so are ioctls the
kernel cannot classify — unknown ioctls fail closed on a pinned inode.
Mutation or removal of the `security.peios.sig` xattr is rejected as
well.

The pin is conservative and one-way. It is set once and cleared only
when the inode is allocated or freed, never while the inode is live.
Updating verified executable content therefore means replacing the
inode — write a new file and `rename()` over it — rather than
modifying it in place.

Unsigned, invalid, unstable, bad-signature and no-match attempts never
pin.

## 3.6.6 Library Signature Verification

With the `lsv` mitigation enabled, `mmap()` with `PROT_EXEC` on a
file-backed mapping verifies the backing file. An unsigned file, an
invalid or unstable one, a bad signature, or no matching key all deny
the mapping with `EACCES`. On success the library's tier is compared
against the loading process's: the image has to dominate the process,
so a Protected/PeiosTcb process can load only PeiosTcb-or-above
libraries. With one key in the table this reduces to "is it signed
with the TCB key?"

The hash covers the **entire file**, not the mapped region — the whole
file is read and hashed even when only part of it is being mapped, so
the signature covers code in sections this particular mapping does not
touch.

`mprotect()` adding `PROT_EXEC` to a mapping that was not already
executable runs the same checks in a fixed order: WXP, then TLP, then
LSV. Anonymous mappings have neither a path nor a signature, so TLP
and LSV skip them and only WXP applies.

Enabling `lsv` on a running process also re-validates its existing
executable mappings before the bit is committed (§3.3.2), so the
mitigation cannot be turned on over already-mapped unsigned code.

## 3.6.7 Revocation

There is none. A signed binary later found to be malicious cannot be
invalidated short of removing it from the filesystem or replacing the
kernel image with a different key. No hash blocklist, no per-key
revocation, and no revocation state of any kind exists.

## 3.6.8 Interaction with other mechanisms

**WXP** is orthogonal: it prevents pages being writable and executable
at once, while LSV prevents unsigned executable pages. A process with
both can execute only signed code in read-only pages.

**FACS** runs independently. A signed binary in a directory the caller
cannot reach is still unreachable — the open is denied before signing
is consulted. Signing determines the PIP level of a binary that is
already being executed; it never grants access to one.

**PIP object protection** consumes the result: once a process carries
a tier from its binary's signature, trust labels on objects are
enforced through the AccessCheck pipeline against the `pip_type` and
`pip_trust` held in the PSB (§3.7).

---

# 3.7 Process Integrity Protection

_Peios / Advanced Peios / PKM / KACS_

> PIP protects objects through trust label ACEs and processes through a dominance test — where it is enforced, and the limits of what it can do.

PIP protects **objects** from insufficiently trusted processes through
trust label ACEs evaluated inside AccessCheck (§3.8.7). It also
protects **processes** — their memory, their execution, their metadata
— from other processes, which is what this section covers.

Every process-to-process operation passes two independent checks, and
both have to succeed.

The **process descriptor check** is an ordinary AccessCheck of the
caller's token against the target's process descriptor (§3.3.3). It
answers *who* may operate on the process, and it is where per-operation
granularity lives — different rights for signals, memory, and
metadata.

The **PIP dominance check** is a direct comparison of the two
processes' PSB fields. It answers *what trust level is required*. It
does not use AccessCheck, does not read a descriptor, and does not
involve the DACL pipeline at all: it is a standalone arithmetic test.

The two are complementary. Object PIP protection stops a non-dominant
process opening authd's private key file; process PIP protection stops
the same process reading the key straight out of authd's memory with
ptrace, or killing authd with a signal.

## 3.7.1 The dominance test

```
pip_dominates(caller_psb, target_psb) -> bool:
    if target_psb.pip_type == None:
        return true   // Unprotected target — any caller dominates.
    return caller_psb.pip_type  >= target_psb.pip_type
       AND caller_psb.pip_trust >= target_psb.pip_trust
```

Both axes are plain unsigned integers compared numerically, not closed
enumerations — the dominance layer would happily order tiers the
signing layer cannot currently produce (§3.3.2). The early return for
an unprotected target is what keeps ordinary processes universally
accessible whatever trust values a caller happens to carry.

Dominance is binary. A caller that does not dominate has no process
access at all, whichever operation was attempted; the descriptor
provides the granularity and PIP is the all-or-nothing gate above it.

That asymmetry with the object model is deliberate. Object access has
natural categories — read, write, execute. Process access does not: a
caller that can ptrace a process can read its memory, inject code, and
effectively become it. Partial process access is not a meaningful
boundary.

## 3.7.2 SeDebugPrivilege

`SeDebugPrivilege` bypasses the descriptor check and never the
dominance check. This holds at every enforcement point, and it is
enforced structurally in two independent places: inside the descriptor
evaluation a PIP-label denial short-circuits *before* the debug
rescue is reached, and the standalone dominance test runs afterwards
with no privilege escape of any kind.

## 3.7.3 Where dominance is enforced

**ptrace**, in every mode. A single successful attach is equivalent to
full compromise of the target — read and write memory and registers,
single-step, inject signals, redirect execution — so a non-dominant
caller is refused whatever the mode. The Linux `__ptrace_may_access`
path is patched to return the LSM's answer directly, so native UID and
capability rules no longer grant where KACS denies. Direct memory
access through `/proc/<pid>/mem`, `process_vm_readv` and
`process_vm_writev` routes through the same check, so one hook covers
every memory-access vector. This is what makes in-memory secrets
genuinely unreachable: a compromised administrator cannot read an
HSM daemon's key material out of its address space.

`PTRACE_TRACEME` inverts the roles — the nominated tracer is the
subject and the caller is the target — and requires `PROCESS_VM_WRITE`
on the caller's own descriptor plus dominance by the nominated tracer.

Mode combinations are validated: the mutually exclusive
`PIDFD_OPEN`, `GETFD` and `PROC_QUERY` flags cannot be combined, and a
request that is neither a read nor an attach, or claims to be both, is
rejected as malformed.

**Signal delivery**, uniformly regardless of signal type. Lifecycle
management of PIP-protected processes therefore has to go through a
process that dominates them — in practice peinit, which runs at the
highest tier.

Signalling within the same process security state is not a boundary
operation, and the exemption is **structural**: it is a pointer
comparison of the two processes' security state, tested before any
descriptor or dominance evaluation. It does not depend on the default
descriptor's self ACE, which is what makes `raise()`, `abort()` and
`pthread_kill()` work for restricted and confined tokens whose
AccessCheck against their own descriptor would fail.

Multi-target sends are evaluated per target by Linux's own iteration,
so the signal reaches the permitted subset and the call succeeds if at
least one delivery happened. POSIX's same-session `SIGCONT` exception
is deliberately absent — the patched `check_kill_permission` returns
the KACS answer before reaching the switch that carried it — so
`SIGCONT` needs `PROCESS_SUSPEND_RESUME` plus dominance like every
other job-control signal, whatever the session.

Kernel-originated signals bypass the whole check, as described in
§3.3.3.

**`pidfd_open()`**, a boundary information query rather than a memory
or attach operation, needs `PROCESS_QUERY_LIMITED` plus dominance.
**`pidfd_getfd()`** maps to `PROCESS_DUP_HANDLE` plus dominance —
extracting a descriptor from another process is a boundary crossing in
its own right.

**`/proc` metadata.** Entries that are already ptrace-gated or
memory-open-gated are covered automatically by the ptrace hook. The
rest would leak information about a protected process, so the
non-ptrace-gated entries carry their own descriptor requirement plus
dominance; §3.3.3 gives the mapping. Entries stricter than a metadata
query, such as `/proc/<pid>/stack`, keep their native hardening and
are not brought under the metadata rule.

Denying access prevents reading inside `/proc/<pid>/` but does not
hide the PID: the directory name is still visible through `getdents`.
Visible-but-inaccessible is the accepted position.

`/proc` is not FACS-managed — it is a virtual filesystem with no
backing store and no xattrs — so enforcement there happens through
direct kernel checks rather than an object-backed FACS path.

**Capability metadata.** `capget()` on the current process, or on a
thread sharing its security state, is not a boundary operation.
Against another process it is a detailed information query needing
`PROCESS_QUERY_INFORMATION` plus dominance.

**Resource limits, scheduler and placement.** Read-only `prlimit`
needs `PROCESS_QUERY_INFORMATION` plus dominance; a limit change needs
`PROCESS_SET_INFORMATION` plus dominance. `setpgid()` needs
`PROCESS_SET_INFORMATION`; `getpgid()` and `getsid()` need
`PROCESS_QUERY_LIMITED`; the scheduler, affinity and I/O priority
queries need `PROCESS_QUERY_INFORMATION`; and the memory-placement
mutations Linux routes through `task_movememory` need
`PROCESS_SET_INFORMATION`. Setting nice, scheduler parameters and I/O
priority all need `PROCESS_SET_INFORMATION` too. Self-directed
versions of all of these are not boundary operations and skip both
checks.

**CPU affinity** is per-thread, so changing the caller's own thread or
a sibling in the same process is not a boundary operation. Changing a
thread in a *different* process needs `PROCESS_SET_INFORMATION` plus
dominance plus `SeIncreaseBasePriorityPrivilege` — and the privilege
is checked and marked used *before* the descriptor and dominance call,
so the `SeDebugPrivilege` rescue cannot substitute for it. KACS does
not relax the kernel's native affinity validity rules: an invalid or
disallowed mask still fails.

**Token opens.** `kacs_open_process_token` and
`kacs_open_thread_token` need `PROCESS_QUERY_INFORMATION` plus
dominance. Reading a process's security identity is as sensitive as
reading its memory.

**Performance monitoring.** Target-specific `perf_event_open()` on
another process can leak execution timing, branch prediction
behaviour, cache access patterns and instruction traces — side
channels that reveal cryptographic keys. It needs
`SeProfileSingleProcessPrivilege` plus `PROCESS_QUERY_INFORMATION`
plus dominance, with the privilege again checked first so the debug
rescue cannot stand in for it. Own-task profiling is not a boundary
operation and needs no privilege. System-wide profiling, `pid == -1`,
samples every task on a CPU including protected ones, so it needs the
operator-class `SeSystemProfilePrivilege`. Cgroup perf mode stays
under Linux's native model. Because the target task is resolved after
the stock `security_perf_event_open` hook fires, this rule is enforced
through a target-resolved syscall patch rather than that hook — there
is no `security_perf_event_open` registration at all.

## 3.7.4 The PeiosTcb floor on kernel-initiated execs

One place PIP gates execution rather than merely labelling it. It is
not a dominance test — there is no caller to compare against — but a
threshold: a binary the kernel execs *on its own behalf* must carry at
least PeiosTcb trust, or the exec fails with `EACCES`.

The case that motivates it is `request_module()`. When the kernel needs
a module it does not have — `get_fs_type()` on a mount, `socket()` for
an unknown protocol family, the crypto API resolving a name — it spawns
`CONFIG_MODPROBE_PATH` as a usermode helper and runs it at the kernel's
own authority. That path is a writable sysctl, which makes redirecting
`/proc/sys/kernel/modprobe` a classic escalation: point it somewhere
attacker-controlled and the next module request executes it with the
kernel behind it. The floor makes the redirection worthless on its own,
because the attacker would also have to produce a TCB-signed binary.

This is the only exec KACS refuses on integrity grounds. Everywhere
else an unsigned binary runs and simply carries no tier, because a
requesting process's own authority bounds what it can do. A
kernel-initiated exec has no such process behind it, so there is no
lesser authority to fall back to.

The floor also refuses when no tier could be derived at all, not only
when one was derived and graded too low. Treating "could not establish
trust" differently from "is not trusted" would leave the check
bypassable by whatever prevented the derivation from running.

Nothing upstream identifies such an exec by the time the LSM sees it.
The helper child is created by `user_mode_thread()`, so it never
carries `PF_KTHREAD` — `kernel_execve()` rejects kernel threads
outright — and `security_kernel_module_request()` fires in the
*requesting* task, before the child exists. So `kernel/umh.c` is
patched to mark the child after `commit_creds()` and before
`kernel_execve()`, and the mark is read in the `bprm_creds_from_file`
hook. It lives in the KACS task blob rather than costing a
`task_struct` flag.

The mark is never cleared. A helper that re-execs — an interpreter for
a `#!` helper — stays under the floor rather than escaping it on the
second exec; the task exists only to be that helper.

A refusal emits `kacs_exec` with reason `umh-not-tcb`, so it is
visible rather than presenting as an unexplained module-load failure.

## 3.7.5 Raw physical memory

A process able to read `/dev/mem` could map any process's physical
pages and bypass virtual memory protections entirely, PIP included.

The defence is `CONFIG_STRICT_DEVMEM`, which restricts `/dev/mem` to
I/O regions and denies RAM access. It is not merely a recommended
build option: the LSM **refuses to initialise** unless both
`CONFIG_STRICT_DEVMEM` and `CONFIG_MODULE_SIG_FORCE` are enabled, so a
kernel configured without them does not boot with KACS at all. The
same initialisation gate refuses to coexist with SELinux, AppArmor,
Smack, TOMOYO or the BPF LSM.

Placing a restrictive descriptor on `/dev/mem` and `/dev/kmem` as a
secondary defence is not implemented; nothing in the kernel handles
those paths specially.

## 3.7.6 Limits of the guarantee

PIP operates inside the kernel's trust boundary, and three things sit
outside it.

**Kernel compromise.** A loaded module runs with unrestricted access
to all memory and kernel structures. PIP is enforced by the kernel, so
a compromised kernel voids it, and `SeLoadDriverPrivilege` is the
ceiling of every guarantee here. `CONFIG_MODULE_SIG_FORCE` is
hard-required as noted above, and module signing is itself ML-DSA-65.
Stripping `SeLoadDriverPrivilege` from every token but peinit's and the
device manager's is the other half of that defence, and is policy
rather than kernel behaviour — nothing in the kernel strips it. The
device manager holds it because loading drivers for the hardware that
appears is its job; module signature enforcement is what keeps the
privilege from meaning more than "load a module Peios built".

**Hardware access.** DMA-capable devices read and write physical
memory directly, bypassing the CPU's virtual memory system. An IOMMU
mitigates this, and configuring one is a kernel responsibility outside
KACS.

**Hypervisor-level isolation.** PIP does not offer guarantees
equivalent to hypervisor-based memory isolation. The threat model
ceiling is a non-compromised kernel.

## 3.7.7 Impersonation

PIP reads the PSB, never the effective token. A Protected service
impersonating a client still evaluates its own PSB for every process
boundary check, so the client's identity has no bearing on it. In the
other direction, an unprotected process impersonating a token created
for a protected one gains nothing — its PSB is still None. Since
nothing constrains the PIP dimensions the way the integrity ceiling
constrains impersonation (§3.5.3), the PSB is the only safe source.

## 3.7.8 Coredumps

A crashing PIP-protected process is a potential secret leak, so its
dumps must not be readable by non-dominant processes.

The implemented strategy is to disable them: a process with a nonzero
`pip_type` has its dumpable flag cleared at exec, and
`prctl(PR_SET_DUMPABLE, 1)` is refused for as long as the process
remains protected. Requests that keep or make it non-dumpable are
allowed, and no alternative dumpable-setting path is left ungated. If
a later exec assigns None/0, normal Linux exec-time dumpability rules
apply to the new image.

The alternative — a signed, high-trust crash handler receiving dump
data from the kernel and writing it under a restrictive descriptor, so
that diagnostics survive without bypassing isolation — is not
implemented. The two are not mutually exclusive; disabling dumps is
the minimum viable position.

---

# 3.8.1 AccessCheck Overview

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> The function that connects tokens to descriptors — its two API variants, and the three state values a right can hold during evaluation.

AccessCheck is the function that connects tokens to security
descriptors. Given a token — who is asking — a descriptor — what the
rules are — and a desired access mask — what they want to do — it
returns a verdict: which of the requested rights are granted, and
whether the request as a whole succeeds.

It is a pipeline, evaluating several layers of policy in a fixed
order. Each layer can grant or constrain access and the layers
interact: integrity policy can block what the DACL would allow,
privileges can override what the DACL denied, and confinement can
revoke what privileges granted. The order is not incidental — it is
the specification.

## 3.8.1.1 Two API variants

**AccessCheck** is the common case. It returns the granted access
mask, whether the request succeeded, a continuous audit mask derived
from SACL alarm ACEs, and a CAAP staging mismatch flag. When an object
type list is supplied, success requires every listed node to pass and
the returned mask is the intersection across them. The staging
mismatch flag is set when the staged scalar result differs from the
effective scalar result, when any per-node staged grant differs from
the effective per-node grant, or when staged auditing differs from
effective auditing.

**AccessCheckResultList** is the per-property variant and requires an
object type list. It returns a separate verdict for each node, so a
denial on one property fails that property alone rather than the whole
request. It returns the same continuous audit mask and staging
mismatch flag, with the flag set when any node's staged granted mask
differs from that node's effective granted mask, or when staged
auditing differs from effective auditing. Directory services use it,
because one operation there may touch several properties with
independent access rules. Privilege-use auditing in this variant takes
the same per-node view: a privilege counts as successfully used if its
contributed bits survive on *any* node's final granted mask.

Both variants share one evaluation pipeline. Only the collection of
results differs.

## 3.8.1.2 The three state values

Every access check tracks three masks.

**`decided`** records which bits have been resolved. It enforces
first-writer-wins within the DACL walk: once a bit is decided, no
later ACE in the same walk changes its outcome. Pipeline layers that
operate on top of the DACL result — restricted token intersection,
confinement intersection, PIP revocation, CAAP intersection — may
still revoke granted bits. Those layers narrow the result; they do not
re-open decided bits for re-evaluation through the DACL.

**`granted`** records which bits resolved to yes. During the walk it
is a subset of `decided`. Afterwards the later layers may remove bits
from it, and the final value is what the caller receives.

**`privilege_granted`** records which bits in `granted` came from a
privilege rather than from the DACL. It exists for two reasons: audit
accuracy, so that privilege-granted access is distinguishable from
DACL-granted access, and the restricted token merge, where
privilege-granted bits are restored after the intersection so that
privileges bypass the restricted pass. PIP may revoke
privilege-granted bits.

With an object type list present, each node carries its own `decided`
and `granted` pair.

---

# 3.8.2 The DACL Walk

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> Walking the ACEs in order under first-writer-wins — SID matching, absent and empty DACLs, owner implicit rights and MAXIMUM_ALLOWED.

The DACL is an ordered list of ACEs, walked from first to last,
comparing each ACE's SID against the calling token's identity. The
governing principle is **first-writer-wins**: once a bit has been
resolved, granted or denied, no later ACE changes the outcome for that
bit.

An **allow** ACE whose SID matches the token grants the rights it
carries that have not yet been decided, leaving already-decided bits
untouched. A **deny** ACE whose SID matches denies its
not-yet-decided rights — marking them decided but not granted — and
likewise leaves decided bits alone.

## 3.8.2.1 SID matching

An ACE's SID matches the token when it equals the user SID or a group
SID on the token, subject to attribute filtering.

For **allow** ACEs, only groups that are enabled and not deny-only
match. For **deny** ACEs, both enabled groups and deny-only groups
match: a deny-only group always participates in deny matching whatever
its enabled state. A group with neither `SE_GROUP_ENABLED` nor
`SE_GROUP_USE_FOR_DENY_ONLY` participates in no matching at all.

The user SID follows the same rule — when `user_deny_only` is set on
the token, it matches deny ACEs and not allow ACEs.

## 3.8.2.2 Skipping and mapping

ACEs carrying `INHERIT_ONLY` exist purely to propagate to child
objects and are skipped by the walk.

At the top of the walk each ACE's access mask is mapped through
`MapGenericBits` using the same GenericMapping applied to the caller's
request. The mapping works on a local copy — the ACE itself is never
mutated. Mapping ACE masks at evaluation time is a deliberate
departure from MS-DTYP, and it is what makes `GENERIC_ALL` work in
central access policy recovery ACEs (§3.8.8).

## 3.8.2.3 Absent and empty DACLs

If the descriptor has no DACL — `SE_DACL_PRESENT` unset — every valid
right not already decided by an earlier pipeline stage is granted. The
valid rights are bounded by `MapGenericBits(GENERIC_ALL, mapping)`
rather than by a raw `0xFFFFFFFF`.

If the DACL is present but holds zero ACEs, the walk grants nothing.
The only access an owner gets in that case comes from the implicit
rights mechanism below.

## 3.8.2.4 Owner implicit rights

By default the owner of an object receives `READ_CONTROL` and
`WRITE_DAC` whatever the DACL says. These are granted **before** the
walk begins, as the first action inside `EvaluateDACL`, and because
first-writer-wins governs the walk, no deny ACE encountered later can
override them.

`EvaluateDACL` takes a `skip_owner_implicit` parameter. The
confinement pass sets it, because confinement is an absolute
intersection with no owner bypass.

The grant is suppressed entirely if any non-inherit-only
access-control ACE in the DACL targets the `OWNER RIGHTS` SID
(`S-1-3-4`). This is a pre-scan performed at the start of
`EvaluateDACL`, before the main loop, and it checks only for the SID's
presence — it does not evaluate any conditional expression on the ACE.

During the walk proper, `S-1-3-4` is treated as an ordinary SID
matching the owner, at both allow and deny polarity. It obeys the same
rules as any other SID: an allow ACE matches only through an enabled,
non-deny-only group, and not through the user SID of a
`user_deny_only` token; a deny ACE matches through a group that is
enabled or deny-only, and through the user SID unconditionally.

Note that the *implicit* grant above is a separate rule and remains
presence-based. It is bounded by the pre-scan rather than by polarity.

The implicit grant is also bounded twice over: by the object type's
valid rights, and by what has already been decided. A pre-decision
from MIC, PIP or a privilege therefore suppresses it — a non-dominant
owner does not receive `WRITE_DAC` through this route.

## 3.8.2.5 MAXIMUM_ALLOWED

When the caller includes `MAXIMUM_ALLOWED` (bit 25), AccessCheck runs
the full pipeline and returns the complete set of rights that would be
granted. The bit is stripped from the desired mask before evaluation
begins.

Two things change. The walk runs to completion with no short-circuit,
and the returned mask is whatever the pipeline accumulated rather than
being filtered to the requested bits.

`MAXIMUM_ALLOWED` can be combined with specific rights:
`MAXIMUM_ALLOWED | READ_CONTROL` asks both "can I read the
descriptor?" as a success or failure and "what else could I get?" as a
mask. A pure `MAXIMUM_ALLOWED` request carrying no specific bits
always succeeds.

Otherwise — when the desired mask is fully decided — the walk may stop
early.

First-writer-wins applies to `MAXIMUM_ALLOWED` requests exactly as it
does to targeted ones. MS-DTYP treats the two differently; KACS does
not, which is what stops "what can I do?" and "can I do this?"
disagreeing on a DACL that is not in canonical order.

---

# 3.8.3 Mandatory Integrity Control

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> MIC restricts what the DACL is allowed to grant along a vertical trust hierarchy — the labels, the policy bits, and the algorithm.

MIC is a mandatory constraint that restricts which rights the DACL is
allowed to grant, along a vertical trust hierarchy. It is evaluated
**before** the DACL walk, in the pre-SACL phase.

Every token carries an integrity level, and every object may carry a
mandatory label — a `SYSTEM_MANDATORY_LABEL_ACE` in its SACL. MIC
compares the two: a caller below the object's level is blocked from
whole categories of access whatever the DACL says.

The default is **no-write-up**. A lower-integrity process can read and
execute a higher-integrity object but cannot write to it, and the
object's label may additionally block reads or execution for callers
beneath it.

An object with no mandatory label ACE in its SACL — or no SACL at all
— is treated as Medium integrity with no-write-up, so Low and
Untrusted processes cannot write to unlabelled objects.

A caller whose level is greater than or equal to the object's label
**dominates** it, and MIC pre-decides nothing: the DACL handles
authorization normally.

## 3.8.3.1 What MIC does and does not touch

MIC constrains what the DACL can grant. It does not revoke what
privileges have already granted, because it mutates only `decided` and
never touches `granted` or `privilege_granted`.

`ACCESS_SYSTEM_SECURITY` is outside its reach for a structural reason:
the bits MIC can decide are bounded by
`MapGenericBits(GENERIC_ALL, mapping)`, which does not include it. The
right is privilege-granted rather than DACL-granted, so MIC never
blocks it. PIP is stricter and does revoke it for non-dominant callers
— explicitly ORing it into the set of bits it can take away — which is
the mechanism by which objects stay protected even from
administrators.

`SeRelabelPrivilege` has one specific interaction: it lets the DACL
grant `WRITE_OWNER` even when an integrity mismatch would otherwise
block it, so a privileged administrator can take ownership of a
higher-integrity object as the first step in modifying it. The bit
granted this way is recorded under its own provenance and is
deliberately **not** part of `privilege_granted`, so it is not
restored after the restricted merge and is not preserved by the CAAP
error escape hatch.

Enforcement is gated on the token's `mandatory_policy`: with
`NO_WRITE_UP` set — the default — the rule applies, and with it clear
MIC is effectively disabled for that token. The field is fixed at
creation (§3.2.2), which is what makes MIC a boundary rather than a
suggestion.

## 3.8.3.2 Labels

An object's SACL may carry more than one mandatory label ACE. Only the
first non-inherit-only one is used; inherit-only labels do not apply
to the object carrying them.

The SID in a mandatory label ACE has the Mandatory Label authority
(`S-1-16`) and exactly one sub-authority, and that sub-authority value
*is* the integrity level, compared as an unsigned integer. Any
`S-1-16-X` is therefore valid.

| SID | Level | Name |
|---|---:|---|
| `S-1-16-0` | 0 | Untrusted |
| `S-1-16-4096` | 4096 | Low |
| `S-1-16-8192` | 8192 | Medium |
| `S-1-16-12288` | 12288 | High |
| `S-1-16-16384` | 16384 | System |

Peios tooling and authd use these five, but intermediate values such
as `S-1-16-2048` or `S-1-16-8448` are valid and compared numerically,
which is what allows Windows-originated descriptors carrying
non-standard levels to be evaluated without translation.

A label ACE whose SID falls outside the `S-1-16` authority — wrong
identifier authority, or the wrong sub-authority count — is
malformed, and so is one that is not a plain single-SID ACE. Either
causes AccessCheck to reject the whole descriptor with an error rather
than ignore the label.

## 3.8.3.3 Policy bits

| Bit | Value | Meaning |
|---|---|---|
| `SYSTEM_MANDATORY_LABEL_NO_READ_UP` | 0x00000001 | Non-dominant callers receive no read-mapped rights from the DACL. |
| `SYSTEM_MANDATORY_LABEL_NO_WRITE_UP` | 0x00000002 | Non-dominant callers receive no write-mapped rights from the DACL. |
| `SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP` | 0x00000004 | Non-dominant callers receive no execute-mapped rights from the DACL. |

Unknown bits in a label mask are ignored.

## 3.8.3.4 The algorithm

```
EnforceMIC(ace, token, mapping, &decided):

    if not (token.mandatory_policy & NO_WRITE_UP):
        return

    token_dominates = (token.integrity_level >= ace.integrity_level)

    if token_dominates:
        return

    // Non-dominant: start with read + execute, strip per label policy.
    allowed = MapGenericBits(GENERIC_READ, mapping)
            | MapGenericBits(GENERIC_EXECUTE, mapping)

    if ace.mask & SYSTEM_MANDATORY_LABEL_NO_READ_UP:
        allowed &= ~MapGenericBits(GENERIC_READ, mapping)
    if ace.mask & SYSTEM_MANDATORY_LABEL_NO_WRITE_UP:
        allowed &= ~MapGenericBits(GENERIC_WRITE, mapping)
    if ace.mask & SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP:
        allowed &= ~MapGenericBits(GENERIC_EXECUTE, mapping)

    // READ_CONTROL and SYNCHRONIZE are always allowed regardless of the
    // object type's GenericMapping and the label's up-strip policy — a
    // non-dominant caller can always read the descriptor and synchronize
    // on the object. Applied after the strips because a file GENERIC_READ
    // mapping folds these bits in, so NO_READ_UP would otherwise take them.
    allowed |= READ_CONTROL | SYNCHRONIZE

    // SeRelabelPrivilege: let WRITE_OWNER through MIC.
    if token.privilege_enabled(SeRelabelPrivilege):
        allowed |= WRITE_OWNER

    all_bits = MapGenericBits(GENERIC_ALL, mapping)
    decided |= all_bits & ~allowed
```

---

# 3.8.4 Restricted Tokens

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> The second pass a restricted token forces — when it runs, how privileges bypass it, and how owner rights and virtual groups behave in it.

A restricted token carries a secondary SID list, the restricting SIDs.
The second pass runs whenever that list or the restricted device group
list is non-empty, and AccessCheck evaluates the DACL twice.

The **normal pass** evaluates the DACL against the token's ordinary
identity — user SID and group SIDs — exactly as usual. The
**restricted pass** evaluates the same DACL again with SID matching
drawn only from the restricting SID list; the token's normal groups
are invisible to it. Conditional membership operators — `Member_of`,
`Member_of_Any`, and their device variants — take the same restricted
view, seeing only the restricting SIDs plus any virtual groups
injected from that list.

The restricting SID list is **presence-based**. A SID participates
whenever it appears in the list, and `SE_GROUP_ENABLED` and
`SE_GROUP_USE_FOR_DENY_ONLY` on those entries are ignored both for
restricted-pass SID matching and for restricted-pass non-device
conditional membership. This matches Windows restricted-token
behaviour, where restricting SIDs are always enabled for access
checks.

Access is granted only for rights **both passes agree on** — the
intersection. The restricting list acts as a ceiling: the principal
can never receive more access than the restricting SIDs would
independently justify.

## 3.8.4.1 Write-restricted tokens

In the write-restricted variant the intersection applies only to
write-category bits, and read and execute access comes from the normal
pass alone. What counts as write is whatever the object type's
GenericMapping maps `GENERIC_WRITE` onto.

## 3.8.4.2 Privileges bypass the restricted pass

Rights granted by privileges are added back after the intersection.
Privileges are system-level grants from security policy rather than
from the object's DACL: token restriction reduces the identity-based
access surface, and privilege-based grants are orthogonal to it. The
bits restored are the post-PIP privilege-granted set together with the
take-ownership grant.

## 3.8.4.3 Owner rights and virtual groups in the restricted pass

The restricted pass evaluates owner implicit rights independently. If
the object's owner SID appears in the restricting SID list, the pass
grants `READ_CONTROL` and `WRITE_DAC`, subject to the same `OWNER
RIGHTS` suppression pre-scan as the normal pass; if the owner SID is
not a restricting SID, no implicit rights are granted.

Two virtual groups are injected on the same basis. `S-1-3-4` (`OWNER
RIGHTS`) is injected when the object's owner SID is among the
restricting SIDs, and `S-1-5-10` (`PRINCIPAL_SELF`) when `self_sid`
is. This keeps the restricted pass consistent in its handling of these
well-known SIDs rather than letting them leak the unrestricted
identity.

Restricted device groups, when the token has them, are swapped in for
the restricted pass so that `Device_Member_of` and its relatives
evaluate against the restricted set rather than the unrestricted
device groups.

---

# 3.8.5 Object ACEs and Property-Level Access

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> Per-property access control through GUID-scoped ACEs — object type lists, propagation, PRINCIPAL_SELF, and per-node results.

An object ACE carries a GUID identifying the property or property set
its rule applies to, which is what enables per-property access control
on objects with internal structure — Active Directory objects, most
obviously. An object ACE with no GUID, meaning
`ACE_OBJECT_TYPE_PRESENT` is unset, behaves exactly like a basic ACE.

## 3.8.5.1 Object type lists

To request property-level access the caller supplies an object type
list: a tree of GUIDs representing the properties being asked for.
Each node carries its own `decided` and `granted` pair and is resolved
independently.

With no list supplied, object ACEs with GUIDs apply globally, as if
they were basic ACEs. With a list supplied, every ACE that behaves
like a basic ACE — ordinary basic ACEs, and object ACEs without an
`ObjectType` GUID — applies to every node in the tree. An object ACE
whose GUID does not appear anywhere in the supplied tree is silently
skipped.

## 3.8.5.2 Propagation

Decisions move through the tree in four ways.

**Downward from grants.** A grant on a property set flows to every
attribute within it, each child node still applying first-writer-wins
for itself.

**Upward from grants.** When every attribute within a property set has
been granted the same right, that right propagates up to the set's
node. The propagation is a per-bit intersection, so a right reaches
the parent only if all siblings share it.

**Upward from denials.** A denial on an attribute propagates to every
ancestor regardless of what its siblings hold, and siblings are
themselves unaffected. Ancestors still apply first-writer-wins to the
propagated bits, so a denial cannot overturn something an ancestor had
already decided.

**Downward from denials.** A denial on a property set flows to every
attribute within it, again subject to first-writer-wins.

## 3.8.5.3 PRINCIPAL_SELF

`PRINCIPAL_SELF` (`S-1-5-10`) is a placeholder for the object's
associated principal. An ACE targeting it matches the caller when the
caller's token represents the same principal as the object, which the
caller establishes by passing the object's principal SID as the
`self_sid` parameter. With a null `self_sid`, `PRINCIPAL_SELF` ACEs
match nothing.

It follows the ordinary deny-only rules: if the caller's matching SID
is deny-only, `PRINCIPAL_SELF` matches deny ACEs but not allow ACEs.

## 3.8.5.4 Scalar and per-node results

`AccessCheck` requires every node to pass, so a denial on any one
property fails the whole request. `AccessCheckResultList` returns a
separate verdict per node.

The scalar result `AccessCheck` returns is the root node's granted
mask. That is equivalent to the intersection across all nodes, but by
construction rather than by computation: upward denial propagation
forces every descendant's denial into all of its ancestors' `decided`
sets, which guarantees the root's granted mask is a subset of every
node's.

## 3.8.5.5 Validation

Object type lists are validated strictly, at parse time. A supplied
list is non-empty; its first node is at level 0; there is exactly one
level-0 node; there are no level gaps, meaning no node at level N+2
following one at level N; and no GUID appears twice.

These checks are stricter than MS-DTYP, which does not specify them.
They exist because a duplicate GUID makes node lookup return the wrong
node, and a level gap makes propagation undefined.

---

# 3.8.6 Application Confinement

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> An absolute boundary a confined token cannot cross whatever the DACL says — normal and strict confinement, and where it sits in the order.

Confinement restricts a token's effective access to what is explicitly
granted to its confinement identity. Even where the normal DACL walk
grants access through the user SID or a group SID, the confinement
pass intersects that with what the confinement SIDs would receive on
their own, and revokes anything they cannot independently justify.

A confined token carries `confinement_sid`, its confinement identity;
`confinement_capabilities`, the capability SIDs the application
declares; and `confinement_exempt`, an escape hatch that skips
confinement evaluation entirely.

The confinement SID set is `confinement_sid` together with every SID
in `confinement_capabilities`. Capabilities are **presence-based**
identities rather than ordinary ACE-matching groups: a capability SID
participates whenever it is present, and disabling an entry or marking
it deny-only does not remove it from the confinement identity.

The pass also injects two confinement-scoped virtual groups.
`S-1-5-10` (`PRINCIPAL_SELF`) is injected only when `self_sid` equals
the confinement SID or one of the capability SIDs, and `S-1-3-4`
(`OWNER RIGHTS`) only when the object's owner SID does. Both apply to
ACE SID matching during the confinement walk *and* to the conditional
membership operators — `Member_of`, `Member_of_Any`, and their device
and negated variants — evaluated during that pass.

## 3.8.6.1 An absolute boundary

Confinement is not overridable. Privileges do not bypass it: the
confinement merge takes no privilege-granted input at all, so backup,
restore, `SeTakeOwnershipPrivilege` and `SeSecurityPrivilege` are
alike unable to grant access the confinement check denies. Owner
implicit rights are skipped entirely, because the pass runs with
`skip_owner_implicit` set.

One thing does pass through: an object with a **null DACL** grants in
the confinement pass exactly as it does in the normal pass. A null
DACL means "no discretionary restrictions", and the confinement pass
follows standard evaluation, granting all valid bits.

## 3.8.6.2 Strict confinement

A normal confined token carries both `ALL_APPLICATION_PACKAGES` and
`ALL_RESTRICTED_APPLICATION_PACKAGES` among its capabilities. Omitting
`ALL_APPLICATION_PACKAGES` gives strict confinement: far fewer system
objects grant to `ALL_RESTRICTED_APPLICATION_PACKAGES`, so the access
surface is much narrower.

Strict confinement is not a separate kernel mode bit. It is derived
purely from the SID set supplied at token creation — if
`ALL_APPLICATION_PACKAGES` is absent, AccessCheck simply evaluates the
remaining confinement SIDs. The kernel never synthesises it, and never
rejects an otherwise valid confined token for carrying it. Deciding
which capabilities a package token receives belongs to authd and
policy tooling.

## 3.8.6.3 Consequences worth stating

**SACL access is unreachable.** `ACCESS_SYSTEM_SECURITY` is only ever
privilege-granted, and privileges do not bypass confinement, so a
confined token cannot reach a SACL unless a confinement ACE grants the
right outright.

**`OWNER RIGHTS` is confinement-scoped.** `S-1-3-4` matches in the
confinement pass only when the owner SID is part of the confinement
SID set.

**`PRINCIPAL_SELF` is isolated from user identity.** `S-1-5-10` is
injected only when `self_sid` matches a confinement SID — the package
SID or one of its capabilities — not when it matches the user.

**Conditional expressions still see the full token.** The confinement
pass isolates ordinary SID matching to the confinement identity, but
conditional expressions inside ACEs continue to evaluate against the
user's real groups and claims. The two confinement-scoped virtual
groups are the only exception, and they follow the confinement rules
during conditional membership evaluation.

## 3.8.6.4 Ordering

Confinement runs **after** the restricted token merge and after its
privilege restoration. The order is load-bearing: privileges bypass
the restricted pass but must not bypass confinement, and if
confinement ran first the privilege restoration would resurrect bits
that confinement had already blocked.

---

# 3.8.7 PIP in AccessCheck

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> How a trust label ACE is evaluated — the label SID, the privileges it revokes, the algorithm, and where the compared values come from.

An object opts in to PIP protection by carrying a
`SYSTEM_PROCESS_TRUST_LABEL_ACE` in its SACL. The ACE's SID encodes
the required type and trust, and its access mask names exactly the
rights a non-dominant caller may still have.

A caller that **dominates** — `pip_type` and `pip_trust` both greater
than or equal to the ACE's — is unrestricted by PIP. A caller that
does not dominate is limited to the ACE mask, and everything else is
denied.

Unlike MIC, PIP has **no default**. An object with no trust label is
unrestricted, reachable by any process whatever its PIP identity.

## 3.8.7.1 The label SID

A trust label SID has the form `S-1-19-{type}-{trust}` — the Process
Trust authority, exactly two sub-authorities. Both axes are compared
numerically. The conventional type values are 0 (None), 512
(Protected) and 1024 (Isolated), but they are labels rather than a
closed enum: any other numeric type is valid and compared by the same
dominance rule.

A trust label SID of any other shape — wrong authority, wrong
sub-authority count — makes the descriptor malformed, and AccessCheck
rejects it outright rather than guessing.

Where a SACL carries more than one trust label ACE, only the first
non-inherit-only one is used; inherit-only labels do not apply to the
object carrying them.

## 3.8.7.2 Privilege revocation

This is the critical difference from MIC. PIP does not merely
constrain what the DACL may grant — it **revokes rights privileges
already granted**. A non-dominant caller who used `SeBackupPrivilege`
to obtain read has those bits stripped; `SeTakeOwnershipPrivilege`'s
`WRITE_OWNER` is stripped; `SeSecurityPrivilege`'s
`ACCESS_SYSTEM_SECURITY` is stripped.

The last of those is the point. Without privilege revocation, a
non-dominant administrator holding `SeSecurityPrivilege` could read
the SACL of a PIP-protected object — and remove the trust label from
it. PIP would be self-defeating.

There is no escape hatch. PIP has no `SeRelabelPrivilege` equivalent,
and no privilege compensates for insufficient trust. It is an absolute
boundary, which is why the enforcement step explicitly ORs
`ACCESS_SYSTEM_SECURITY` into the set of bits it can take away —
that right is outside the generic mapping and would otherwise escape.

## 3.8.7.3 The algorithm

```
EnforcePIP(ace, pip_type, pip_trust, mapping, &decided,
           &granted, &privilege_granted):

    // pip_type and pip_trust are the subject's process-trust context,
    // not a token field. See "Where the values come from" below.

    ace_type  = ace.sid.pip_type
    ace_trust = ace.sid.pip_trust

    caller_dominates = (pip_type  >= ace_type
                    and pip_trust >= ace_trust)

    if caller_dominates:
        return

    // Non-dominant: the ACE mask IS the allowed set.
    allowed = MapGenericBits(ace.mask, mapping)

    // Everything not explicitly allowed is denied, including
    // ACCESS_SYSTEM_SECURITY.
    all_bits = MapGenericBits(GENERIC_ALL, mapping)
             | ACCESS_SYSTEM_SECURITY
    pip_denied = all_bits & ~allowed

    decided |= pip_denied

    // Revoke privilege-granted rights.
    granted           &= ~pip_denied
    privilege_granted &= ~pip_denied
```

## 3.8.7.4 Where the values come from

`pip_type` and `pip_trust` are the subject's process trust context and
are never derived from a token field — the token structure has no PIP
field at all. They are passed into AccessCheck as explicit parameters
by whichever layer is enforcing.

During enforcement — FACS file access, process boundaries — the values
are the subject process's PSB, set at exec from the binary's signature
(§3.6, §3.7).

The `kacs_access_check` query may instead supply them through its
arguments, per axis: zero means "use the calling process's PSB value"
and a nonzero value evaluates against the supplied context. This lets
a userspace broker evaluate access under a client's trust level, in
the same way the query's token argument lets it evaluate a token other
than its own. The query is advisory and gates nothing in the kernel;
enforcement always uses the PSB. The same effective values are used
for the verdict and for the event the check emits, so an audit record
never disagrees with the decision it describes.

The enforcement step also computes a record of which bits PIP decided.
Nothing currently consumes it — it is threaded through three
structures and exported, and no caller reads it.

---

# 3.8.8 Central Access and Auditing Policy

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> Policy defined once and referenced by objects — the policy structure, access and audit evaluation, the cache, and the recovery policy.

CAAP separates policy definition from the objects it governs. A policy
is defined once, centrally; objects reference it by SID through a
`SYSTEM_SCOPED_POLICY_ID_ACE` in their SACL. When the policy changes,
every future AccessCheck against a referencing object uses the new
rules — already-open handles are unaffected, because the model is
check-at-open.

It extends the Windows Central Access Policy model by adding an audit
component: each rule carries both an access restriction and an audit
requirement.

## 3.8.8.1 Policy structure

A policy is a named collection of rules identified by a policy SID.
Each rule carries:

An optional **applies-to condition**, a conditional expression
determining whether the rule governs this decision. It may reference
the `@Resource`, `@User`, `@Device` and `@Local` claim namespaces, but
it cannot inspect SID or device-group membership: `Member_of`,
`Member_of_Any`, `Device_Member_of`, `Device_Member_of_Any` and their
negated forms all evaluate to UNKNOWN inside `applies_to`. A rule with
no condition applies to every object referencing the policy.

A mandatory **effective DACL** — a real DACL evaluated through the
full pipeline. An optional **effective SACL**, whose audit ACEs merge
with the object's own during the audit walk. And optional **staged**
DACL and SACL, proposed replacements used for testing.

## 3.8.8.2 Access evaluation

The DACL result is ANDed with the normal evaluation. CAAP can only
restrict, never expand: if the object's DACL grants read and write but
the applicable rule's effective DACL grants only read, the result is
read.

A SACL may carry several scoped policy ACEs, which makes policies
composable — and the AND semantics are what make composition safe,
since each additional policy can only narrow. Inherit-only scoped
policy ACEs do not apply to the object carrying them and are ignored
during lookup. MS-DTYP allows one scoped policy ACE per SACL; KACS
allows several.

For each scoped policy ACE, AccessCheck looks the policy up in the
kernel cache, then for every rule whose `applies_to` is TRUE or absent
evaluates the rule's effective DACL through the full pipeline —
privilege grants, MIC, PIP, the DACL walk, restricted tokens,
confinement — and intersects the result with the running total, which
starts at the normal evaluation's granted mask.

A rule whose condition evaluates FALSE **or UNKNOWN** is skipped. That
is deliberately the opposite of the deny-ACE UNKNOWN rule: skipping is
the conservative choice here, because a rule's DACL can only narrow
what the normal DACL already granted.

If no rules apply — every condition false or unknown, or the policy
has no rules — CAAP has no effect and the normal result stands.

The rule's DACL is evaluated with backup and restore intent **not**
passed: intent is a caller concern, not a policy concern. And CAAP
never recurses. A rule's synthetic descriptor has scoped policy ACEs
stripped before evaluation, so nested CAAP evaluation cannot occur
even when the original SACL carried more of them. The synthetic
descriptor keeps the original owner and group, and preserves the MIC
and PIP labels, so a rule is evaluated against the same mandatory
constraints as the object itself.

## 3.8.8.3 Audit evaluation

For each applicable rule carrying an effective SACL, the rule's audit
ACEs are evaluated alongside the object's own during the audit walk.
They are treated identically — if either says to audit an operation,
it is audited.

The SACL component is purely additive. A CAAP SACL can add audit
coverage and can never suppress auditing the object's own SACL asks
for.

## 3.8.8.4 The policy cache

The kernel keeps a map from policy SID to policy object, empty at
boot. Policies are pushed in through `kacs_set_caap`, which requires
`SeTcbPrivilege` — and marks it used. A non-null spec for an existing
SID replaces the policy; a null spec or zero length removes it. The
policy SID's length is bounded to 8–68 bytes before parsing begins,
and until the cache has been initialised both setting and evaluating
fail with `EACCES`.

The wire format is:

```
[version:u8 = 0x01]
[rule_count:u32le]
per rule:
  [applies_to_len:u32le][applies_to_expr bytes]     (0 = no condition)
  [effective_dacl_len:u32le][effective_dacl bytes]  (MUST NOT be 0)
  [effective_sacl_len:u32le][effective_sacl bytes]  (0 = no audit rules)
  [staged_dacl_len:u32le][staged_dacl bytes]        (0 = no staged DACL)
  [staged_sacl_len:u32le][staged_sacl bytes]        (0 = no staged SACL)
```

All lengths are little-endian `u32`. ACLs use the standard binary
format, and every ACE type valid in a DACL or SACL is permitted inside
a policy ACL. The `applies_to` expression is conditional ACE bytecode,
carrying the same `artx` prefix as callback ACE application data.

The limits are a spec of at most 256 KB, at most 256 rules, an
`applies_to` of at most 64 KB, and an individual ACL of at most 64 KB.
The version byte has to be `0x01`. Trailing bytes after the declared
rules are rejected.

Validation is strict and total. Malformed or truncated `applies_to`
bytecode fails the whole call with `EINVAL` at ingestion rather than
being admitted and later treated as a runtime UNKNOWN — the structural
check happens once, at the boundary. A rule with a zero-length
effective DACL fails the same way, as do truncated fields, lengths
exceeding the buffer, and invalid ACL headers. `SeTcbPrivilege` is
checked before any parsing begins.

authd populates the cache — from the registry on a standalone machine,
from Active Directory on a domain-joined one. The kernel neither knows
nor cares about the source; it is a passive cache. Policies pushed
after services are running do not retroactively affect handles already
opened.

## 3.8.8.5 The recovery policy

When a scoped policy ACE names a SID that is not in the cache — authd
failed to push it, the policy was deleted, the machine is
disconnected — a hardcoded recovery policy is used instead: `GENERIC_ALL`
to BUILTIN\Administrators, to SYSTEM, and to `OWNER RIGHTS`.

Those masks are stored as the literal `GENERIC_ALL` bit rather than
pre-mapped object-specific bits, and are expanded through the caller's
GenericMapping at evaluation time, so the recovery policy works
correctly for every object type.

Because CAAP is an intersection, recovery does not widen access beyond
the object's own DACL: it limits missing-policy access to callers who
also satisfy the recovery DACL. This is a fail-closed recovery mode
with administrator, SYSTEM and owner escape hatches — not a no-effect
fallback.

## 3.8.8.6 Errors

A rule whose DACL evaluation errors denies everything except rights
granted by privileges. Preserving those is the escape hatch: an
administrator with `SeSecurityPrivilege` keeps the ability to read and
modify the SACL and remove the offending scoped policy ACE.

The escape hatch is conditional, though, and the ordering is why. PIP
runs before CAAP and may already have stripped
`ACCESS_SYSTEM_SECURITY` from the privilege-granted set for a
non-dominant caller. The hatch therefore only works for callers who
are PIP-dominant, or where the object carries no trust label.

Rule evaluation swallows every error kind, including allocation
failure, so an out-of-memory condition inside a rule is reported as
"this rule denied all except privileges" rather than as `ENOMEM`.

A rule whose SACL evaluation errors has its audit contribution
skipped, and a diagnostic event is emitted.

## 3.8.8.7 Staging

A rule may carry staged DACLs and SACLs alongside its effective ones,
and AccessCheck evaluates both in parallel. The staged result affects
neither access nor audit; where effective and staged differ, the
difference is reported through a staging mismatch flag returned to the
caller and a diagnostic event.

A rule with no staged DACL contributes its effective result to both
running totals, and a rule with no staged SACL contributes its
effective SACL to both.

---

# 3.8.9 Auditing in AccessCheck

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> Auditing is purely observational and never changes a decision — access, continuous and privilege-use auditing, and per-token policy.

Auditing is purely observational. No audit rule affects the access
decision, and audit ACEs are evaluated after the decision is final.

Three mechanisms operate inside the pipeline, emitting two families of
KMES record: `access-audit` for object-access events from the SACL
walk and from token audit-policy forcing, and `privilege-use` for
privilege-use events. A third family, `caap-policy-diagnostic`, is
emitted for the CAAP conditions of §3.8.8 — a SACL evaluation error,
or a staged-versus-effective mismatch. The event type strings and
payload schemas are in §3.C.

Event delivery happens before any result is written back to the
caller, and a failure to deliver fails the call. An audit event cannot
be suppressed by handing the syscall a bad output pointer.

## 3.8.9.1 Access auditing

`SYSTEM_AUDIT` ACEs in the SACL define which attempts to log. Each
carries a SID, an access mask, and success and failure flags —
`SUCCESSFUL_ACCESS_ACE_FLAG` (0x40) and `FAILED_ACCESS_ACE_FLAG`
(0x80).

An event is emitted when the ACE's SID matches the caller, its mask
overlaps the requested access, and its flags match the outcome.

Two details matter. The SID is matched with **deny polarity** — the
broadest identity view, in which deny-only groups are visible —
because auditing should capture the widest possible picture rather
than the narrowest. And the overlap is tested against the
generic-mapped **requested** mask, not the final granted mask, so a
failed request is still auditable for the rights it actually asked
for.

Conditional audit ACEs gate the event on an expression, using the same
deny-side membership polarity. An expression evaluating to UNKNOWN
emits the event: when in doubt, audit.

## 3.8.9.2 Continuous auditing

Access auditing fires once, where AccessCheck runs. Continuous
auditing covers per-operation monitoring.

`SYSTEM_ALARM` ACEs configure it. When AccessCheck evaluates an alarm
ACE whose SID matches, the ACE's mask is accumulated into a
**continuous audit mask** returned to the caller, which stores it on
the open handle and enforces it per operation. Conditional alarm ACEs
use the same deny-side polarity as conditional audit ACEs. The alarm
branch deliberately performs no overlap test against the requested
mask — an alarm ACE contributes its mask on a SID match alone.

On each later operation the enforcement point emits a
`continuous-audit` event when the operation's normalised
required-access mask overlaps the stored mask. For FACS handles that
is the same mask used by the use-time check (§3.9.4). Where an
operation's authorization accepts any one of several rights — append
or write data, say — the required mask holds the accepted set and the
event records the subset that overlapped.

Events are emitted after the per-operation decision is known, for
successful and denied attempts alike. The subject and process recorded
are the **operation-time** effective token and current task, not
necessarily the ones that opened the handle. That keeps attribution
correct after a handle is passed between processes, while still using
the opener-computed mask to decide whether the handle is audited at
all.

An enforcement point that cannot construct a required continuous-audit
event fails closed. Transport buffering and drop accounting remain
KMES's concern (§2.7).

## 3.8.9.3 Privilege-use auditing

When a privilege is exercised to grant access the DACL would not have
granted independently, a privilege-use event may be emitted. This runs
after the complete pipeline — after integrity policy, confinement and
central access policy — so it reflects the final result rather than an
intermediate one.

**Successful** privilege use means the privilege's contributed bits
survive into the final granted result. The privilege is marked used,
and an event is emitted when the token's `audit_policy` carries
`PRIVILEGE_USE_SUCCESS` (0x04). **Failed** privilege use means the
privilege contributed bits during evaluation that did not survive. The
privilege is *not* marked used, and an event is emitted under
`PRIVILEGE_USE_FAILURE` (0x08). A privilege that contributed nothing
to the requested access produces no event either way.

With an object type list, the test is per-node: a privilege counts as
successfully used if its bits survive on *any* node's final mask.

A `MAXIMUM_ALLOWED` request short-circuits this stage entirely,
recording no used bits and emitting no privilege-use events at all.

How counterfactual the accounting really is varies by privilege, as
§3.4.1 describes: `SeSecurityPrivilege` and `SeTakeOwnershipPrivilege`
contribute only where the DACL had not already granted the right,
while backup and restore seed their bits unconditionally and so also
report use for accesses the DACL alone would have permitted.

## 3.8.9.4 Per-token audit policy

A token's `audit_policy` can force events regardless of SACL content.
This runs after the SACL walk and before result computation: if the
access succeeded and the policy carries `OBJECT_ACCESS_SUCCESS`
(0x01), a success event is emitted; if it failed and the policy
carries `OBJECT_ACCESS_FAILURE` (0x02), a failure event is.

Success here means every requested bit was granted, or that nothing
was requested at all.

These events are additive — they fire even when no SACL ACE matched —
and they carry the `object_audit_context` the caller supplied. The
policy is per-token, fixed at creation, and follows impersonation,
since it is read from the effective token.

## 3.8.9.5 Event contents

An event carries the **subject**, the calling token's identity — user
SID, group SIDs, integrity level, PIP identity; the **object**, as the
caller-provided context; the **access**, meaning what was requested,
what was granted, and whether the request succeeded; the **trigger**,
which audit ACE matched or which privilege was exercised; and the
**process**, its PID, name and executable path.

The pipeline itself produces only the object-and-access half — the
matched ACE bytes, the requested and granted masks, the outcome,
whether the event was policy-forced, the privilege, and the audit
context. The subject and process halves are attached at emission time
from the resolved call context, which is also where the effective PIP
values used for the verdict are reused for attribution.

---

# 3.8.10 The Algorithm

_Peios / Advanced Peios / PKM / KACS / AccessCheck_

> How every layer composes — the definitive statement of the evaluation order, from EvaluateSecurityDescriptor down through AccessCheckCore.

The preceding sections describe what each layer of AccessCheck does.
This one describes how they compose, and is the definitive statement
of the evaluation order.

## 3.8.10.1 Pipeline overview

Before the pipeline proper, two things are checked and can fail the
call outright. The token's own invariants are validated — a
`write_restricted` token without `user_deny_only` is rejected as
invalid — and, in the orchestrator, a null descriptor is rejected and
`AccessCheckResultList` is required to have been given an object type
list. A null descriptor presented by an Identification-level token
therefore fails as an invalid parameter rather than as access denied,
because the null check runs first.

The pipeline then runs in this order:

0. **Impersonation level gate.** An impersonation token at
   Identification level is denied immediately. Anonymous tokens
   proceed through the full pipeline.
1. **Input validation.** Reject a descriptor with no owner. A null
   group SID is valid and has no direct effect on the decision.
2. **Generic mapping.** Map generic bits in the desired mask to
   object-specific bits; strip `MAXIMUM_ALLOWED`.
3. **Effective privileges.** Clear the backup and restore bits when
   the corresponding intent flag is absent.
4. **Privilege grants.** Resolve `ACCESS_SYSTEM_SECURITY`, backup and
   restore. Seed `decided`, `granted` and `privilege_granted`.
5. **Pre-SACL walk.** Extract the mandatory integrity label, the PIP
   trust label, resource attributes and scoped policy SIDs from the
   SACL, then enforce MIC and PIP.
6. **Virtual group resolution.** `S-1-3-4` and `S-1-5-10` become
   matchable where the caller is the owner or the object's principal.
7. **Tree initialisation.** Seed each node from the scalar state.
8. **Normal DACL evaluation.** Owner implicit rights, then the walk.
9. **Post-DACL WRITE_OWNER override.** `SeTakeOwnershipPrivilege`
   grants `WRITE_OWNER` if the DACL did not and no mandatory
   mechanism blocked it.
10. **Restricted token pass**, with intersection and privilege
    restoration.
11. **Confinement pass**, with absolute intersection.
12. **CAAP.** Evaluate each applicable rule's DACL through the full
    per-descriptor pipeline and intersect; collect SACLs.
13. **Privilege-use auditing.**
14. **Audit emission**, over the object's SACL and any CAAP SACLs.
15. **Result computation.**

Object type lists are validated at parse time rather than at step 1 —
non-empty, one level-0 node first, no level gaps, no duplicate GUIDs —
so a malformed list never reaches the pipeline. Step 1 re-checks only
emptiness.

Reserved access-mask bits (`0x0CE0_0000`) are rejected wherever a mask
is mapped. That applies to the caller's desired mask *and* to every
ACE mask, so a single ACE carrying a reserved bit aborts the entire
check rather than being skipped.

## 3.8.10.2 EvaluateSecurityDescriptor

Steps 0–11, called once for the normal evaluation and once per CAAP
rule with a synthetic descriptor.

```
EvaluateSecurityDescriptor(
    sd, token, pip_type, pip_trust, desired, mapping,
    object_tree, self_sid, local_claims, privilege_intent
) -> (decided, granted, privilege_granted,
      max_allowed_mode, mapped_desired, resource_attributes,
      policy_sids) | error

    // Step 0: Impersonation level gate.
    if token.token_type == Impersonation
       and token.impersonation_level == Identification:
        return ERROR_ACCESS_DENIED

    // Step 1: Input validation.
    if sd.owner is null:
        return ERROR_INVALID_SECURITY_DESCR

    // Step 2: Generic mapping.
    desired = MapGenericBits(desired, mapping)
    max_allowed_mode = (desired & MAXIMUM_ALLOWED) != 0
    desired = desired & ~MAXIMUM_ALLOWED

    // Step 3: Effective privileges.
    effective_privileges = token.privileges_enabled
    if not (privilege_intent & BACKUP_INTENT):
        effective_privileges &= ~SeBackupPrivilege
    if not (privilege_intent & RESTORE_INTENT):
        effective_privileges &= ~SeRestorePrivilege

    // Step 4: Privilege-based grants.
    decided = 0; granted = 0; privilege_granted = 0

    // ACCESS_SYSTEM_SECURITY is always decided by privilege.
    decided |= ACCESS_SYSTEM_SECURITY
    if (effective_privileges & SeSecurityPrivilege):
        granted           |= ACCESS_SYSTEM_SECURITY
        privilege_granted |= ACCESS_SYSTEM_SECURITY

    if (effective_privileges & SeBackupPrivilege):
        backup_bits = MapGenericBits(GENERIC_READ, mapping)
        decided |= backup_bits; granted |= backup_bits
        privilege_granted |= backup_bits

    if (effective_privileges & SeRestorePrivilege):
        restore_bits = MapGenericBits(GENERIC_WRITE, mapping)
                     | WRITE_DAC | WRITE_OWNER | DELETE
                     | ACCESS_SYSTEM_SECURITY
        decided |= restore_bits; granted |= restore_bits
        privilege_granted |= restore_bits

    // Restore already includes WRITE_OWNER, so when it is active
    // step 9 has nothing left to do. Step 9 is the fallback for
    // when restore is inactive and the DACL did not grant it.

    // Step 5: Pre-SACL walk. mandatory_decided records bits decided
    // by MIC and PIP, so step 9 cannot override them.
    resource_attributes = {}; policy_sids = []; mandatory_decided = 0
    PreSACLWalk(sd, token, pip_type, pip_trust, mapping,
                &decided, &granted, &privilege_granted,
                &mandatory_decided, &resource_attributes,
                &policy_sids)

    // Steps 6-8: owner implicit rights are granted first, inside
    // EvaluateDACL, and the tree is seeded from the already
    // augmented scalar state. Virtual groups are resolved per
    // lookup rather than by building an enriched token.
    EvaluateDACL(sd, token, mapping, object_tree,
                 SidMatchesToken, desired, max_allowed_mode,
                 resource_attributes, local_claims,
                 skip_owner_implicit=false,
                 &decided, &granted)

    // Step 9: Post-DACL WRITE_OWNER override.
    if (desired & WRITE_OWNER) != 0 or max_allowed_mode:
        if (effective_privileges & SeTakeOwnershipPrivilege):
            if not (mandatory_decided & WRITE_OWNER)
               and not (granted & WRITE_OWNER):
                decided |= WRITE_OWNER
                granted |= WRITE_OWNER
                privilege_granted |= WRITE_OWNER
                for each node with WRITE_OWNER ungranted:
                    node.decided |= WRITE_OWNER
                    node.granted |= WRITE_OWNER

    // Step 10: Restricted token pass.
    if token.restricted_sids or token.restricted_device_groups:
        // Restricted identity view: only restricting SIDs, plus
        // S-1-3-4 if the owner is among them and S-1-5-10 if
        // self_sid is. Restricted device groups swap in.
        // Fresh tree, zeroed state.
        EvaluateDACL(sd, restricted_view, mapping, r_tree,
                     SidInRestrictingSids, desired,
                     max_allowed_mode, resource_attributes,
                     local_claims, skip_owner_implicit=false,
                     &r_decided, &r_granted)
        if token.write_restricted:
            write_bits = MapGenericBits(GENERIC_WRITE, mapping)
            granted = (granted & ~write_bits)
                    | (granted & r_granted & write_bits)
        else:
            granted = granted & r_granted
        granted |= privilege_granted        // privileges bypass
        // Same intersection and restoration per node.

    // Step 11: Confinement pass.
    if token.confinement_sid and not token.confinement_exempt:
        // Confinement SID set: confinement_sid plus every
        // capability, presence-based. S-1-3-4 and S-1-5-10 are
        // injected only if the owner or self_sid is in that set.
        EvaluateDACL(sd, token, mapping, c_tree,
                     SidInConfinementSids, desired,
                     max_allowed_mode, resource_attributes,
                     local_claims, skip_owner_implicit=true,
                     &c_decided, &c_granted)
        granted = granted & c_granted       // no privilege bypass
        // Same absolute intersection per node.

    return (decided, granted & root-consistent state,
            privilege_granted narrowed to what survived,
            max_allowed_mode, desired, resource_attributes,
            policy_sids)
```

The returned `privilege_granted` is **narrowed** by what actually
survived the pass — it is intersected with the root's granted mask on
return, and the orchestrator narrows it again against the CAAP result.
A privilege-granted bit that the write-restricted merge or the
confinement intersection removed is therefore no longer part of it,
which matters in two places: the CAAP error escape hatch (§3.8.8) has
fewer bits to preserve, and the audit provenance masks reflect the
narrowed set.

## 3.8.10.3 AccessCheckCore

The orchestrator runs steps 12 through 15.

**Step 12, CAAP.** For each scoped policy SID, look the policy up —
falling back to the recovery policy when it is absent — and for each
rule whose `applies_to` is TRUE or absent, evaluate the rule's
synthetic descriptor through `EvaluateSecurityDescriptor` and
intersect. A rule that errors denies everything except the (already
narrowed) privilege-granted bits. Staged DACLs are evaluated in
parallel into a separate running total; a rule with no staged DACL
contributes its effective result to both.

After all policies, the staged and effective totals are compared and
any difference sets the staging mismatch flag. In result-list mode a
per-node delta sets it too — and so does a scalar delta, since the
comparison is not mode-branched.

**Step 13, privilege-use auditing.** For each of the five provenance
masks — security, backup, restore, take-ownership and relabel:

```
success_bits = provenance & mapped_desired & granted
failure_bits = provenance & mapped_desired & ~granted
```

Nonzero `success_bits` means the privilege was load-bearing: mark it
used on the token, and emit a success event under
`PRIVILEGE_USE_SUCCESS`. Otherwise nonzero `failure_bits` means it was
exercised but did not survive: do **not** mark it used, and emit a
failure event under `PRIVILEGE_USE_FAILURE`. Both zero means no event.
In result-list mode the comparison folds across nodes — success if the
bits survive on any node, failure only if they survive on none.

The whole step is skipped in `MAXIMUM_ALLOWED` mode, so such a request
marks nothing used and emits nothing.

Note that `relabel_granted` is tracked as provenance but is
deliberately excluded from `privilege_granted` itself, so a
relabel-loosened `WRITE_OWNER` is neither restored after the
restricted merge nor preserved by the CAAP error hatch.

**Step 14, audit emission.** Walk the object's SACL, then each CAAP
effective SACL, accumulating audit events and ORing alarm masks into
the continuous audit mask. This is read-only with respect to
`granted`.

The staged comparison then walks the object's SACL again followed by
the staged SACLs. That second walk is driven by the **staged** granted
total rather than the effective one, so the success and failure
classification of staged audit events reflects the staged access
result — which is what makes the flag sensitive to descriptors whose
staged and effective grants differ.

**Step 14b, forced auditing.** With
`success = (granted & mapped_desired) == mapped_desired or
mapped_desired == 0`, the token's `audit_policy` forces a success or
failure event additively, regardless of what the SACL matched.

**Step 15** returns the accumulated state.

## 3.8.10.4 The wrappers

`AccessCheck` takes the **root node's** granted mask when a tree is
present, then computes `allowed` as `mapped_desired == 0` or every
requested bit granted. The root's mask equals the intersection across
all nodes by construction rather than by computation: upward denial
propagation (§3.8.5) forces every descendant's denial into all of its
ancestors, so the root can never grant what a descendant denies.

`AccessCheckResultList` requires a tree and returns a per-node granted
mask and status, each node judged against `mapped_desired`
independently.

Neither wrapper filters the returned `granted` to the requested mask.
Privilege seeding at step 4 ORs bits in regardless of what was asked
for, so a caller that requested only `READ_CONTROL` while holding
backup can see read bits it never requested. The file enforcement path
does filter its result; the generic query path does not.

## 3.8.10.5 Helpers

```
SidMatchesToken(sid, token, for_allow) -> bool
    if sid == token.user_sid:
        if for_allow and token.user_deny_only:
            return false
        return true
    for group in token.groups:
        if not group.enabled and not group.deny_only:
            continue
        if for_allow and group.deny_only:
            continue
        if sid == group.sid:
            return true
    return false
```

```
MapGenericBits(mask, mapping) -> ACCESS_MASK
    if mask & RESERVED_BITS: reject
    mapped = mask & ~(GENERIC_READ | GENERIC_WRITE
                    | GENERIC_EXECUTE | GENERIC_ALL)
    if mask & GENERIC_READ:    mapped |= mapping.read
    if mask & GENERIC_WRITE:   mapped |= mapping.write
    if mask & GENERIC_EXECUTE: mapped |= mapping.execute
    if mask & GENERIC_ALL:     mapped |= mapping.all
    return mapped
```

All four generic bits are cleared before any is expanded, so a mask
naming several generics maps every one of them.

**Virtual group resolution.** There is no enrichment step producing a
modified token. `S-1-3-4` and `S-1-5-10` are resolved at each lookup
instead, in the DACL walk, the SACL walk and conditional membership
alike. `S-1-5-10` resolves through the ordinary polarity rules against
`self_sid`, and `S-1-3-4` through the ordinary polarity rules against
the object's owner SID. Both are computed once per walk — the owner is
fixed while the polarity is per ACE — so each carries its allow and
deny answers together.

**`EvaluateSACL`** walks in a fixed order per ACE: SID match with deny
polarity, then object-type scoping against the tree, then the
condition, then the mask overlap against `mapped_desired`. Audit ACEs
need all four; alarm ACEs deliberately skip the overlap test and
contribute their mask on a SID match alone. Inherit-only ACEs are
skipped throughout.

**`synthetic_sd`** builds a CAAP rule's descriptor from the original's
owner and optional group with the rule's DACL substituted, and the
SACL copied with every scoped policy ACE stripped — which is what
prevents recursion. The MIC and PIP labels are preserved, so a rule is
evaluated under the same mandatory constraints as the object. The
control bits and the stripped SACL's revision are recomputed rather
than copied.

## 3.8.10.6 Provenance masks

| Variable | Set at | Meaning |
|---|---|---|
| `security_granted` | Step 4 | `SeSecurityPrivilege` granted `ACCESS_SYSTEM_SECURITY`. |
| `backup_granted` | Step 4 | `SeBackupPrivilege` granted read bits. |
| `restore_granted` | Step 4 | `SeRestorePrivilege` granted write and metadata bits. |
| `take_ownership_granted` | Step 9 | `SeTakeOwnershipPrivilege` granted `WRITE_OWNER`. |
| `relabel_granted` | Step 5 | `SeRelabelPrivilege` added `WRITE_OWNER` to the MIC allowed set. |

Each records which bits that privilege contributed, and step 13
compares each against the requested mask and the final result.

---

# 3.9.1 The Handle Model

_Peios / Advanced Peios / PKM / KACS / FACS_

> FACS is the file-specific enforcement surface of KACS — mount policy, scope, how a handle is acquired, and stacked backing files.

FACS, the File Access Control Shim, is the file-specific enforcement
surface of KACS. It replaces Linux DAC — UID, GID and mode-bit checks
— with security-descriptor evaluation on files.

Enforcement follows the handle pattern. AccessCheck runs once at open
time, the granted mask is cached on the file description, and later
operations test the cached mask:

```
(fd.granted & required) == required ? allow : deny
```

The mask is set once and never modified, and it is immutable for the
descriptor's whole lifetime regardless of any later descriptor change.
A file's DACL can be rewritten while a process holds it open, and that
process keeps the rights it was granted. A few operations use a live
AccessCheck instead; §3.9.4 lists them.

A second immutable mask is stamped alongside it: the **continuous
audit mask** (§3.8.9), which every use-time operation consults to
decide whether to emit a per-operation audit event. It travels with
the granted mask through every path described below.

## 3.9.1.1 Mount policy

Every mounted filesystem exposes exactly one FACS mount-policy class,
scoped to the kernel superblock object rather than to a pathname or a
bind mount — several paths or bind mounts over one superblock all
observe the same class.

**`unmanaged`** puts the mount outside the handle model entirely: no
granted mask is stamped on its file descriptions. **`facs_deny_missing`**
is FACS-managed and denies access where a descriptor is missing.
**`facs_synthesize_ephemeral`** synthesises missing descriptors and
caches them in memory only. **`facs_synthesize_persistent`**
synthesises them and writes them back immediately. The three `facs_*`
classes are the only managed ones.

The default classifier is conservative. Hardcoded pseudo-filesystems —
`/proc`, `/sys`, nullfs — are `unmanaged`. Filesystems that cannot
reliably store the canonical descriptor xattr are
`facs_synthesize_ephemeral`: FAT, exFAT, NFS client mounts, ISO9660,
and cgroup2. StrataFS is fixed at `facs_deny_missing`. Everything else
— tmpfs, squashfs, ext4, btrfs — defaults to `facs_deny_missing`
unless a trusted policy agent adopts it.

Userspace cannot set a superblock to `unmanaged` through the public
ABI; the class is reserved for the kernel classifier and the hardcoded
rules. Attempting to set a policy on a magic-derived `unmanaged`
superblock fails with `EOPNOTSUPP`, and so does attempting to change
StrataFS's.

## 3.9.1.2 Scope

The sole-authority claim covers local FACS-managed filesystems. It
does not cover `O_PATH` descriptors, which are not managed; NFS client
mounts, which are ephemeral-synthesising and retain dual authority
with the server; `/proc`, which is unmanaged and PIP-protected; or
`/sys`, which is unmanaged and carries a hardcoded rule instead —
writes there require Administrators or SYSTEM, enforced against a
built-in descriptor. Descriptors for processes obtained through
`pidfd` bypass FACS at open as well.

## 3.9.1.3 Handle acquisition

A descriptor carries its granted mask through every acquisition path,
and transfer is an intentional capability-delegation mechanism.
`dup`/`dup3` and `fork` produce the same open file description and
therefore the same rights; descriptors without `FD_CLOEXEC` survive
exec unchanged; `SCM_RIGHTS` transfers the descriptor as a capability
token, with possession as authorization and no re-check of the
receiver's identity; and `pidfd_getfd()` is gated by an AccessCheck
for `PROCESS_DUP_HANDLE` against the target process, after which the
caller receives the descriptor with its full mask.

The security boundary is at open time. Every subsequent transfer
carries the mask unchanged.

This means mandatory subject policy — MIC and PIP — is evaluated once,
at open, against the opener. A high-integrity process that opens a
file and passes the descriptor to a low-integrity one has effectively
delegated its access. That is the handle model: authority is on the
handle, not on the holder.

## 3.9.1.4 Stacked backing files

When a stacking filesystem creates a kernel-private backing file for a
managed user-visible file, the outer file's granted and continuous
audit masks are captured into the backing-file blob, and the backing
file's ordinary blob receives that exact snapshot when the provider is
opened. No new AccessCheck runs against the task executing the
stacking filesystem.

The inheritance is deliberately narrow. It is admitted only through
the kernel's typed backing-file allocation path, only from a managed
non-`O_PATH` outer file, and it retains no reference to that outer
file — only the values. It is not a general snapshot-cloning
mechanism.

An active StrataFS copy-up context takes precedence: a backing open
that does not match its exact object and phase does not fall back to
inherited authority. When an exactly-bound copy-up backing file is
adopted by the outer descriptor, the outer snapshot is installed into
both blobs as a single transition, after which the backing file
behaves like any other stacked open.

The user-visible operation is checked and continuously audited once,
against the outer handle. Immediate provider re-entry through
`security_file_permission` or `security_mmap_file` neither repeats the
caller authorization nor emits a second caller audit event. The
backing file keeps the snapshot for later descriptor-local
enforcement, including `mprotect()` after a stacked mmap, and the
`mmap_backing_file` handoff verifies that the outer, backing and
captured snapshots still agree before the provider mapping is
installed.

This suppresses duplicate *KACS* authorization only. It bypasses
nothing else — not another LSM, not filesystem errors, not read-only
mounts, immutable state, quota, space, I/O, or format validation.

---

# 3.9.2 KACS-Native Open

_Peios / Advanced Peios / PKM / KACS / FACS_

> Opening a file by naming every right up front — the required data right, directories, special nodes, create dispositions and the DELETE fallback.

`kacs_open` takes an explicit desired access mask. The caller names
every right it will need — `FILE_READ_DATA`, `FILE_WRITE_DATA`,
`WRITE_DAC`, `READ_CONTROL`, any combination — and AccessCheck
evaluates the whole requested mask at open. If every requested right
is granted the descriptor's mask is set to the requested mask; if any
is denied, the open fails. This is the **strict** mode, in contrast to
the legacy path's subset behaviour (§3.9.3).

`MAXIMUM_ALLOWED` may be combined with at least one concrete data
right or `FILE_EXECUTE`. The concrete bits define the Linux `f_mode`
of the returned descriptor and have to be granted; `MAXIMUM_ALLOWED`
then makes the cached mask the full computed maximum rather than the
requested set. Alone it is invalid, because it defines no file mode,
and fails with `EINVAL`.

## 3.9.2.1 The required data right

Every native open names at least one data right — `FILE_READ_DATA`,
`FILE_WRITE_DATA`, `FILE_APPEND_DATA` — or `FILE_EXECUTE`, so that
every descriptor has a valid mode. `FILE_READ_DATA` maps to
`FMODE_READ`; either write right maps to `FMODE_WRITE`; and
`FILE_EXECUTE` alone maps to `FMODE_EXEC`, which enables
`execveat(fd, "", ..., AT_EMPTY_PATH)` — an execute-only handle that
can neither read nor write the file's contents.

## 3.9.2.2 Directories

Directory rights share bit positions with file data rights:
`FILE_LIST_DIRECTORY` is `FILE_READ_DATA` (0x0001), `FILE_ADD_FILE` is
`FILE_WRITE_DATA` (0x0002), and `FILE_ADD_SUBDIRECTORY` is
`FILE_APPEND_DATA` (0x0004). A native directory open with
`FILE_LIST_DIRECTORY` satisfies the data-right requirement and maps to
`FMODE_READ`.

The two write aliases are not cacheable directory handle rights,
though. `kacs_open` rejects a desired mask naming `FILE_ADD_FILE`,
`FILE_ADD_SUBDIRECTORY` or `FILE_DELETE_CHILD` on a directory with
`EOPNOTSUPP`. Those are parent-directory authorization rights for
namespace operations, evaluated live at the operation, not rights a
handle carries.

## 3.9.2.3 Special nodes and symlinks

Existing FIFOs, pathname socket nodes, and character and block device
nodes on managed mounts are ordinary filesystem file objects for
authorization: the file object type, the file right mapping, and the
file GenericMapping for metadata, standard and generic rights.

`FILE_EXECUTE` is not a valid data substitute for them — a
`kacs_open` naming it on such a node fails closed. The Linux object
implementation may still impose its own device- or
filesystem-specific denial after KACS has authorized the file object.

Symlink objects are file objects for `kacs_get_sd`, `kacs_set_sd` and
`readlink`, but they are not opened as terminal objects by
`kacs_open`. Following is the default resolution behaviour, and
`AT_SYMLINK_NOFOLLOW` fails with `ELOOP`. This differs deliberately
from `kacs_get_sd` and `kacs_set_sd`, where the same flag resolves the
symlink object itself.

Operations needing neither data nor execute access — changing a
DACL without reading contents — use path-based interfaces or `O_PATH`
descriptors as object anchors, which the get and set security calls
accept through `AT_EMPTY_PATH`.

## 3.9.2.4 Create dispositions

| Value | Name | If it exists | If it does not |
|---|---|---|---|
| 0 | `FILE_SUPERSEDE` | Delete and recreate | Create |
| 1 | `FILE_OPEN` | Open | Fail |
| 2 | `FILE_CREATE` | Fail | Create |
| 3 | `FILE_OPEN_IF` | Open | Create |
| 4 | `FILE_OVERWRITE` | Truncate to zero | Fail |
| 5 | `FILE_OVERWRITE_IF` | Truncate to zero | Create |

`FILE_SUPERSEDE` deletes the existing name and creates a new file
under it, requiring `DELETE` on the existing file — or
`FILE_DELETE_CHILD` on the parent — together with `FILE_ADD_FILE` on
the parent. The new file gets a new inode and a new descriptor,
inherited or caller-supplied. The superseded pathname is broken away
from the old hardlink set and names the new inode; other pre-existing
hardlinks continue to name the old inode, and already-open descriptors
still reference it.

`FILE_OVERWRITE` truncates in place — same inode, same descriptor,
hardlinks preserved — and requires `FILE_WRITE_DATA`.

On an `unmanaged` mount, `FILE_OPEN` and `FILE_OPEN_IF` succeed and
return an unmanaged descriptor with no stamped mask rather than
failing; the creating dispositions fail with `EOPNOTSUPP`.

### 3.9.2.4.1 The DELETE fallback

Both `FILE_SUPERSEDE` and delete-on-close reference "`DELETE` on the
file, or `FILE_DELETE_CHILD` on the parent". That is a two-descriptor
check inside the open path: AccessCheck runs first against the target
file for `DELETE`, and if that is not granted it runs again against
the parent directory for `FILE_DELETE_CHILD`. If neither grants, the
open fails. The same duality governs link operations.

## 3.9.2.5 Create options

| Value | Name | Description |
|---|---|---|
| 0x0001 | `KACS_CREATE_OPT_DIRECTORY` | The target has to be a directory. When creating, create a directory rather than a regular file; when opening an existing non-directory, fail with `ENOTDIR`. |
| 0x0002 | `KACS_CREATE_OPT_DELETE_ON_CLOSE` | Delete the file when the last handle in its lineage closes. Requires `DELETE` on the file or `FILE_DELETE_CHILD` on the parent. |

All other bits are reserved and have to be zero; a nonzero reserved
bit fails with `EINVAL`.

Delete-on-close is deliberately **no-share**. `kacs_open_how` carries
no share-mode field, so the Windows `FILE_SHARE_DELETE` compatibility
matrix is not representable in the frozen ABI, and the kernel contract
is bounded instead. The obligation attaches to one ordinary file
description lineage rather than to Linux inode last-reference
semantics, so `dup()`, `fork()` and `SCM_RIGHTS` all preserve it
because they preserve the description. Once a lineage exists for a
file object, later opens of that object fail closed rather than
emulating share-mode compatibility. The unlink happens at final close
of that lineage — not at open, and not at generic inode
last-reference drop — and if the pathname is already gone by then the
close path treats it as a no-op rather than a new error. Regular files
only: directory delete-on-close fails closed.

## 3.9.2.6 Caller-supplied descriptors

When a disposition results in a new file the caller may supply a
descriptor. A null pointer with zero length means the new file's
descriptor is inherited from the parent directory through the
inheritance algorithm.

Supplying one on a branch that opens an *existing* object is invalid
input rather than something silently ignored. `FILE_OPEN_IF`
resolving to an existing object fails with `EINVAL`, and so do
`FILE_OVERWRITE` and the existing-object branch of
`FILE_OVERWRITE_IF`, since those retain the existing inode and its
descriptor. `FILE_OPEN` with a descriptor supplied fails with
`EOPNOTSUPP`.

For a genuine creation the kernel computes the new object's descriptor
**first**, then runs the strict native-open AccessCheck against *that*
descriptor for the requested access. Parent create rights authorize
namespace creation; they do not by themselves authorize the returned
handle. A failed strict check rolls the creation back — the newly
created file or directory is removed — and the syscall fails.

Creator-supplied descriptors are validated at create time. An owner
SID has to be the caller's own or a token group marked
`SE_GROUP_OWNER`, unless `SeRestorePrivilege` is enabled, in which
case any owner is allowed. A supplied SACL is treated as a full SACL
input rather than a label-only fragment, and therefore requires
`SeSecurityPrivilege`. If that SACL carries an explicit mandatory
label ACE, the label also has to satisfy the ordinary label-write
constraint: at or below the caller's integrity level, unless
`SeRelabelPrivilege` is held.

## 3.9.2.7 Modes and status

`kacs_open_how` carries no POSIX mode field, so the raw Linux inode
mode for native creation is fixed: `0600` for regular files, `0700`
for directories. These are compatibility metadata only — and if Linux
DAC ever denies an operation KACS would have authorized, the operation
fails closed.

Native creation does not create FIFOs, socket nodes, device nodes or
symlinks. Those stay on the Linux namespace APIs — `mknod()`,
`mkfifo()`, Unix `bind()` to a path — governed by the namespace hooks.
NTFS is excluded from native creation entirely.

The syscall reports what happened: created, opened, overwritten, or
superseded. `KACS_STATUS_SUPERSEDED` is used only when an existing
object was actually replaced — a `FILE_SUPERSEDE` that finds no target
and simply creates one reports `KACS_STATUS_CREATED`.

---

# 3.9.3 Legacy Open Compatibility

_Peios / Advanced Peios / PKM / KACS / FACS_

> Linux open flags cannot express WRITE_DAC or READ_CONTROL — how FACS maps them to core and compat rights, and what O_PATH does.

Linux's `open()` and `openat()` cannot express rights like `WRITE_DAC`
or `READ_CONTROL`. FACS maps the open flags to a **core** set of
required rights plus a **compat** set of POSIX-expected ones, and
evaluates both in a single AccessCheck.

## 3.9.3.1 Core rights

The core set is the minimum for a usable descriptor, and the open
fails if any of it is denied.

For regular files, device nodes, FIFOs and pathname sockets:

| Flags | Core rights |
|---|---|
| `O_RDONLY` | `FILE_READ_DATA \| FILE_READ_ATTRIBUTES` |
| `O_WRONLY` | `FILE_WRITE_DATA \| FILE_READ_ATTRIBUTES` |
| `O_RDWR` | `FILE_READ_DATA \| FILE_WRITE_DATA \| FILE_READ_ATTRIBUTES` |

For directories, `O_RDONLY` gives
`FILE_READ_ATTRIBUTES | FILE_TRAVERSE`. Directory core deliberately
excludes `FILE_LIST_DIRECTORY`, so a directory opened `O_RDONLY` can
be used for `fchdir()` and `fstat()` without listing permission;
listing is a compat right.

Two modifiers apply in order. `O_APPEND` replaces `FILE_WRITE_DATA`
with `FILE_APPEND_DATA`, and `O_TRUNC` adds `FILE_WRITE_DATA`. Given
both, the replacement happens first and the re-addition second, so
core ends up with `FILE_APPEND_DATA` and `FILE_WRITE_DATA` together.

`FILE_READ_ATTRIBUTES` is always core: a descriptor granting data
access but denying attribute reads is not openable through the legacy
APIs at all.

## 3.9.3.2 Compat rights

Requested alongside core and silently omitted where denied:
`FILE_READ_EA` for `fgetxattr()`; `READ_CONTROL` for reading the
descriptor; `FILE_WRITE_ATTRIBUTES` for `futimens()`; `FILE_WRITE_EA`
for `fsetxattr()`; `FILE_WRITE_DATA` on `O_APPEND` opens so
`ftruncate()` works where the descriptor allows it; `WRITE_DAC`,
because POSIX permits `fchmod()` on any descriptor; `WRITE_OWNER`, for
`fchown()` on the same grounds; `SYNCHRONIZE`;
`FILE_LIST_DIRECTORY`, enabling `readdir()` on directory descriptors;
and `FILE_EXECUTE`, enabling `fexecve()` on regular files.

## 3.9.3.3 The flow

The full requested mask is core plus compat. AccessCheck returns the
subset the descriptor allows. If every core right is present the
actual granted mask — which may include all, some or none of compat —
is stamped on the descriptor; otherwise the open fails with `EACCES`.

Both open paths run the same pipeline with different success
criteria. Native open is **strict**: everything requested has to be
granted. Legacy open is **subset**: only core has to be fully present.

## 3.9.3.4 O_PATH

`O_PATH` descriptors are not FACS-managed. The open hook does fire for
them, but returns immediately without evaluating anything, so they
carry no granted mask and are left unmanaged. They serve as namespace
anchors for the `*at()` syscalls.

`fstat()` and `fstatfs()` on them are allowed unconditionally.
`fchdir()` runs a live `FILE_TRAVERSE` check at use time. `fchmod()`,
`fchown()`, `fgetxattr()`, `fsetxattr()`, `ioctl()` and `mmap()` are
denied with `EBADF` — though `futimens()` currently has no such guard.
`execveat(fd, "", ..., AT_EMPTY_PATH)` has exec permission enforced by
a live AccessCheck in the bprm hook, and `kacs_get_sd` and
`kacs_set_sd` with `AT_EMPTY_PATH` likewise run live, which gives
race-free object identity without snapshot authorization.

That `fstat()` is unconditional means `FILE_READ_ATTRIBUTES` is not
authoritative for attribute confidentiality. In practice size,
timestamps and inode number are rarely confidential. The descriptor
itself *is* protected: `kacs_get_sd` on an `O_PATH` handle performs a
live check.

---

# 3.9.4 Use-Time Semantics

_Peios / Advanced Peios / PKM / KACS / FACS_

> Every operation on an open descriptor is a mask check against the granted mask — data, metadata, traversal, append-only, fcntl and ioctl.

Every operation on an open descriptor is a mask check against the
granted mask, with one exception: `execveat(AT_EMPTY_PATH)` uses a
live AccessCheck.

## 3.9.4.1 Data operations

| Operation | Required right |
|---|---|
| Read | `FILE_READ_DATA` |
| Sequential write, no append intent | `FILE_WRITE_DATA` |
| Append-intent write (`O_APPEND` descriptor or `RWF_APPEND`) | `FILE_APPEND_DATA` or `FILE_WRITE_DATA` |
| Positioned write, or a no-append override | `FILE_WRITE_DATA` — denied on append-only descriptors |
| Directory listing | `FILE_LIST_DIRECTORY` |
| `ftruncate` | `FILE_WRITE_DATA` |
| `fallocate` allocation (`ALLOCATE_RANGE`, with or without `KEEP_SIZE`) | `FILE_APPEND_DATA` or `FILE_WRITE_DATA` |
| `fallocate` mutation (`PUNCH_HOLE`, `ZERO_RANGE`, `COLLAPSE_RANGE`, `INSERT_RANGE`, `UNSHARE_RANGE`, `WRITE_ZEROES`) | `FILE_WRITE_DATA` |
| `mmap PROT_READ` | `FILE_READ_DATA` |
| `mmap PROT_WRITE \| MAP_SHARED` | `FILE_WRITE_DATA` — `FILE_APPEND_DATA` alone is insufficient |
| `mmap PROT_WRITE \| MAP_PRIVATE` | `FILE_READ_DATA` — copy-on-write, no write to the file |
| `mmap PROT_EXEC` | `FILE_EXECUTE` |
| `mprotect` | As `mmap`, for the new protection flags |
| `flock LOCK_SH` / `F_RDLCK` | `FILE_READ_DATA` |
| `flock LOCK_EX` / `F_WRLCK` | `FILE_WRITE_DATA` or `FILE_APPEND_DATA` |
| `fsync` / `fdatasync` | `SYNCHRONIZE` |

A `fallocate` mode outside the supported set fails closed, and
`PUNCH_HOLE` additionally requires `KEEP_SIZE`.

## 3.9.4.2 Metadata operations

| Operation | Required right |
|---|---|
| `stat` / `lstat` / path `statx` | `FILE_READ_ATTRIBUTES` |
| `fstat` / descriptor `statx` | `FILE_READ_ATTRIBUTES` |
| `fstatfs` | `FILE_READ_ATTRIBUTES` |
| path and descriptor `file_getattr` | `FILE_READ_ATTRIBUTES` |
| path and descriptor `file_setattr` | `FILE_WRITE_ATTRIBUTES` |
| `truncate` by pathname | `FILE_WRITE_DATA` |
| `chmod` / `fchmodat` / `fchmod` | `WRITE_DAC` |
| `chown` / `lchown` / `fchownat` / `fchown` | `WRITE_OWNER` |
| `utimensat` / `utimes` / `futimens` | `FILE_WRITE_ATTRIBUTES` |
| `getxattr` / `lgetxattr` / `fgetxattr` | `FILE_READ_EA` |
| `setxattr` / `lsetxattr` / `removexattr` / `fsetxattr` / `fremovexattr` | `FILE_WRITE_EA` |
| `listxattr` / `llistxattr` / `flistxattr` | none |
| `access` / `faccessat` `F_OK` | `FILE_READ_ATTRIBUTES` |
| `access` / `faccessat` `R_OK` | `FILE_READ_DATA` |
| `access` / `faccessat` `W_OK` | `FILE_WRITE_DATA` |
| `access` / `faccessat` `X_OK` | `FILE_EXECUTE` |

Reads and writes of the canonical descriptor xattr are denied
unconditionally through the xattr hooks — `security.peios.sd`, or
`system.ntfs_security` on NTFS. All descriptor access goes through
`kacs_get_sd` and `kacs_set_sd`. POSIX ACL xattr writes are denied
unconditionally too, with `EOPNOTSUPP` rather than `EACCES` so that
probe-then-tolerate callers behave sensibly.

## 3.9.4.3 Directory traversal

Path resolution checks `FILE_TRAVERSE` on managed directory
components. A token holding `SeChangeNotifyPrivilege` bypasses the
intermediate checks, including on directories whose descriptor is
missing.

Explicit changes of the current or root directory are not intermediate
resolution: `chdir()` and `chroot()` take a live `FILE_TRAVERSE` check
on the final directory, and the privilege bypass does not apply
(§3.4.2). An ordinary `fchdir()` checks the descriptor's cached mask;
an `O_PATH` `fchdir()` runs live.

## 3.9.4.4 Append-only enforcement

A handle carrying `FILE_APPEND_DATA` but not `FILE_WRITE_DATA` allows
only true append-intent writes. Append intent means the effective
write position is forced to end-of-file by `O_APPEND` or per-I/O
`RWF_APPEND`, and is not negated by `RWF_NOAPPEND` on the same
operation. Requesting both `RWF_APPEND` and `RWF_NOAPPEND` together
fails with `EACCES`.

Denied on such a handle: positioned writes without effective append
intent — `pwrite64`, `pwritev`, `pwritev2` with an explicit offset,
io_uring writes with an explicit offset, and AIO writes with an
offset; any write using `RWF_NOAPPEND`, since it can negate append
semantics inherited from `O_APPEND`; shared writable `mmap` and
`mprotect` upgrades to `PROT_WRITE`; and the `fallocate` mutation
modes.

## 3.9.4.5 fcntl

For `F_SETFL`, KACS evaluates the mutable status flags Linux accepts —
`O_APPEND`, `O_NONBLOCK`/`O_NDELAY`, `O_DIRECT`, `O_NOATIME`.
Clearing `O_APPEND` is denied on a handle with `FILE_APPEND_DATA` but
not `FILE_WRITE_DATA`; setting it is always allowed, being a privilege
reduction. Adding `O_NOATIME` requires `FILE_WRITE_ATTRIBUTES` and
clearing it is always allowed. Changing only `O_NONBLOCK`, `O_NDELAY`
or `O_DIRECT` needs no KACS right, though ordinary Linux validation
still applies.

These commands are descriptor-local and require no KACS right, and
none of them widens the cached mask: `F_CREATED_QUERY`; `F_DUPFD`,
`F_DUPFD_CLOEXEC` and `F_DUPFD_QUERY`, which preserve the same file
description and mask; `F_GETFD` and `F_SETFD`; `F_GETFL`; and the
async-notification set `F_GETOWN`, `F_GETOWN_EX`, `F_GETOWNER_UIDS`,
`F_GETSIG`, `F_SETOWN`, `F_SETOWN_EX` and `F_SETSIG`, where Linux pid
and signal validation still applies.

These are object-state queries or mutations, checked against the
cached mask before Linux-specific validation:

| Command | Required right |
|---|---|
| `F_GETLK` / `F_GETLK64` / `F_OFD_GETLK` | Any data right |
| `F_GETLEASE` / `F_GETDELEG` | `FILE_READ_ATTRIBUTES` |
| `F_GETPIPE_SZ` | `FILE_READ_ATTRIBUTES` |
| `F_SETPIPE_SZ` | `FILE_WRITE_ATTRIBUTES` |
| `F_GET_SEALS` | `FILE_READ_ATTRIBUTES` |
| `F_ADD_SEALS` | `FILE_WRITE_ATTRIBUTES` |
| `F_GET_RW_HINT` / `F_GET_FILE_RW_HINT` | `FILE_READ_ATTRIBUTES` |
| `F_SET_RW_HINT` / `F_SET_FILE_RW_HINT` | `FILE_WRITE_ATTRIBUTES` |

Lock, lease and delegation commands — `F_SETLK`, `F_SETLKW`,
`F_SETLK64`, `F_SETLKW64`, `F_OFD_SETLK`, `F_OFD_SETLKW`,
`F_SETLEASE`, `F_SETDELEG` — pass through the fcntl hook so that the
later file-lock hook can enforce them against normalised `F_RDLCK`,
`F_WRLCK` or `F_UNLCK` values. An unknown lock type fails closed
there.

For `F_NOTIFY`, removing a watch — a zero event mask, ignoring
`DN_MULTISHOT` — requires nothing. Installing one with any known `DN_*`
event requires `FILE_LIST_DIRECTORY`. Unknown `DN_*` bits on a managed
descriptor fail closed.

Unmanaged descriptors sit outside the handle check entirely, and an
unknown fcntl command on a managed one fails closed.

## 3.9.4.6 ioctl

Known ioctls are classified by required right; an unclassified one is
allowed if the descriptor carries at least one data right. The 32-bit
compat aliases take the same right as their native command, including
`FS_IOC32_GETFLAGS`, `FS_IOC32_SETFLAGS`, `FS_IOC32_GETVERSION`,
`FS_IOC32_SETVERSION` and the compat preallocation commands.

**Descriptor-local**, requiring nothing: `FIOCLEX` and `FIONCLEX`,
which change close-on-exec state; `FIONBIO`, which changes nonblocking
state; and `FIOASYNC`, which changes async notification state with
Linux and fops validation still applying.

**Common VFS:**

| ioctl | Required right |
|---|---|
| `FIBMAP` | `FILE_READ_DATA` |
| `FIGETBSZ` | `FILE_READ_ATTRIBUTES` |
| `FIFREEZE` / `FITHAW` | `FILE_WRITE_ATTRIBUTES` |
| `FITRIM` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_GETFSUUID` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_GETFSSYSFSPATH` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_GETLBMD_CAP` | `FILE_READ_ATTRIBUTES` |

`FIFREEZE`, `FITHAW` and `FITRIM` mutate filesystem operational
state, and Linux's own `CAP_SYS_ADMIN` checks still apply on top.

**File and object:**

| ioctl | Required right |
|---|---|
| `FS_IOC_FIEMAP` | `FILE_READ_DATA` |
| `FIONREAD` | `FILE_READ_DATA` on a regular file; any data right otherwise |
| `FS_IOC_GETFLAGS` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_SETFLAGS` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_GETVERSION` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_SETVERSION` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_RESVSP` / `FS_IOC_RESVSP64` | `FILE_APPEND_DATA` or `FILE_WRITE_DATA` |
| `FS_IOC_UNRESVSP` / `FS_IOC_UNRESVSP64` | `FILE_WRITE_DATA` |
| `FS_IOC_ZERO_RANGE` | `FILE_WRITE_DATA` |
| `FICLONE` / `FICLONERANGE` | `FILE_WRITE_DATA` |
| `FIDEDUPERANGE` | `FILE_WRITE_DATA` |
| `FIOQSIZE` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_FSGETXATTR` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_FSSETXATTR` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_GETFSLABEL` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_SETFSLABEL` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_GET_ENCRYPTION_PWSALT` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_GET_ENCRYPTION_POLICY` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_GET_ENCRYPTION_POLICY_EX` | `FILE_READ_ATTRIBUTES` |
| `FS_IOC_SET_ENCRYPTION_POLICY` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_ADD_ENCRYPTION_KEY` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_REMOVE_ENCRYPTION_KEY` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS` | `FILE_WRITE_ATTRIBUTES` |
| `FS_IOC_GET_ENCRYPTION_KEY_STATUS` | `FILE_READ_ATTRIBUTES` |
| `BLKGETSIZE64` | `FILE_READ_ATTRIBUTES` |
| `BLKFLSBUF` | `FILE_WRITE_DATA` |

On directories, `FS_IOC_GETFLAGS` and `FS_IOC_SETFLAGS` take the same
rights as on files.

Anything unclassified is allowed on any data right. For device nodes,
pipes and sockets, device-specific ioctl semantics are outside FACS
scope: the node's descriptor is the authorization boundary, and
Linux's device-specific validation may still deny.

A pinned inode (§3.6) narrows all of this. Every content-, range- or
allocation-mutating ioctl is rejected on one, and so is every ioctl
the classifier does not recognise — unknown ioctls fail closed there
rather than falling back to the data-right rule.

## 3.9.4.7 Execution

Execution is nominally a two-layer check. The **mode execute bit** is
the prerequisite meaning "this file is a program", set by package
managers and `chmod +x`, applying to `execve` and `execveat` but not
to `mmap(PROT_EXEC)`. The **descriptor's `FILE_EXECUTE`** is the
access control, gating both.

KACS enforces only the second. Nothing in the kernel module tests the
execute mode bit; the prerequisite survives because Linux's own
`generic_permission()` refuses `MAY_EXEC` on a file with no execute
bit set, by way of `capable_wrt_inode_uidgid(CAP_DAC_OVERRIDE)`. The
`+x` requirement is therefore a Linux DAC property that KACS inherits
rather than a FACS rule, and it is one of the few decisions where mode
bits still matter (§3.10.2).

For descriptor-based exec — `execveat` with `AT_EMPTY_PATH`, including
on `O_PATH` handles — a live AccessCheck for `FILE_EXECUTE` runs
against the re-opened file rather than the cached mask being consulted.

---

# 3.9.5 File Descriptor Storage

_Peios / Advanced Peios / PKM / KACS / FACS_

> Where a file's descriptor lives and why raw xattr access is denied in all three directions — plus caching, mount policy classes and boot artifacts.

## 3.9.5.1 Xattr protection

FACS intercepts every raw xattr operation on the canonical descriptor
xattr — `security.peios.sd`, or `system.ntfs_security` on NTFS — and
denies all three directions.

**Writes** are denied: all modification goes through the set-security
interface. **Removal** is denied: a descriptor is never detached from
a file. And **reads** are denied, which is the least obvious of the
three and the most important. The raw xattr holds the entire
descriptor including the SACL, so allowing a read under
`READ_CONTROL` alone would leak SACL content that properly requires
`ACCESS_SYSTEM_SECURITY`. All reads go through `kacs_get_sd`, which
distinguishes the two.

## 3.9.5.2 Caching

A validated, parsed descriptor object is cached in the inode's LSM
blob, holding immutable self-relative bytes together with a
prevalidated component layout — enough for AccessCheck readers never
to reparse untrusted storage bytes.

**Readers** on the AccessCheck path use the RCU-published pointer.
Once a current entry exists, a reader does not take the inode mutex
merely to run AccessCheck. It either completes the evaluation inside
an RCU read-side critical section, or pins the object with a refcount
while still under RCU and drops the RCU lock before doing anything
that can allocate, sleep or emit an audit event. A pin is acquired
with a non-zero refcount check and dropped afterwards.

**Writers** allocate a new object, swap the pointer atomically, and
free the old one after a grace period and after reader pins have
drained. No partial read is possible.

**Population** is lazy, on first access. The xattr is read through an
internal kernel path that bypasses the read-denial hook, and the
parsed result is installed by compare-and-swap; a thread that loses
the race frees its own copy.

**Eviction** frees the cached descriptor when the inode is evicted,
through an RCU-safe callback with the same pin draining, so in-flight
permission checks complete before the object goes away.

**Invalidation** happens on write, but not atomically with it. The
set-security path deliberately releases the inode security lock across
the xattr write and re-acquires it afterwards to publish the new
parsed object, because holding it across the write would invert the
`i_rwsem` ordering the access path requires. Two concurrent
set-security calls on one inode are therefore last-writer-wins rather
than serialised end to end, and there is a window in which the xattr
and the cache disagree. Readers are never blocked, and no reader sees
a partially written object — the exposure is which of two racing
writes lands, not a torn state.

## 3.9.5.3 Mount policy classes

The superblock policy object carries the class (§3.9.1) and, for
synthesise-class mounts, an optional mount-level default template.

The default classifier maps from the superblock's filesystem magic.
`PROC_SUPER_MAGIC` and `SYSFS_MAGIC` are **unmanaged**: these expose
kernel state through inode-shaped handles with no on-disk identity and
no descriptor to consult. `NULL_FS_MAGIC` is unmanaged too — nullfs is
the immutable, permanently empty filesystem the kernel mounts as the
mount-namespace root, with the mutable rootfs mounted on top of it. It
declares no xattr support at all, so it can never carry a descriptor,
and its single root inode is immutable and childless: nothing to stamp
and nothing to protect.

`STRATAFS_SUPER_MAGIC` is fixed at **`facs_deny_missing`** for the
superblock's lifetime, because StrataFS delegates every check to
current provider objects and must never synthesise a descriptor for
its merged namespace. An attempt to change it fails with
`EOPNOTSUPP`.

`RAMFS_MAGIC`, `NFS_SUPER_MAGIC`, `MSDOS_SUPER_MAGIC`,
`EXFAT_SUPER_MAGIC`, `ISOFS_SUPER_MAGIC` and `CGROUP2_SUPER_MAGIC` are
**`facs_synthesize_ephemeral`** — either no persistent backing at all,
or storage with no native descriptor slot.

Everything else, including `TMPFS_MAGIC`, `SQUASHFS_MAGIC`,
`EXT4_SUPER_MAGIC` and `BTRFS_SUPER_MAGIC`, defaults to
**`facs_deny_missing`**. These can all carry the descriptor xattr
natively and are expected to on every inode that participates in
access checks.

`TMPFS_MAGIC` covers both userspace tmpfs mounts and the kernel-mounted
instances established before any userspace runs. The latter are not
exempt from the default; they are handled by seeding.

## 3.9.5.4 Kernel-internal mounts

Two filesystems are mounted by the kernel before any userspace process
exists and before anything can call `kacs_set_mount_policy` or
`kacs_set_sd`: the mutable root filesystem mounted by
`init_mount_tree`, a tmpfs mounted on top of the immutable nullfs
namespace root and made `/` by `set_fs_root`; and the devtmpfs
instance mounted by `devtmpfs_init` and populated by the `kdevtmpfs`
thread.

Both are `TMPFS_MAGIC` and therefore `facs_deny_missing`, and their
root inodes are kernel-created, never passing through a
userspace-supplied artifact, so they carry no descriptor at the moment
they become reachable. To make the class viable the kernel seeds one.

The rootfs root is seeded inside `init_mount_tree`, immediately after
`vfs_kern_mount` returns and before the mount is published into
`init_mnt_ns`, with the inode's `i_rwsem` held. The devtmpfs root is
seeded inside `devtmpfs_init`, after `vfs_kern_mount` and before
`kdevtmpfs` starts, likewise under `i_rwsem`. The nullfs root is not
seeded — it is unmanaged, empty, and incapable of xattr storage.

The seeded descriptor is byte-for-byte identical in both places: owner
and group SYSTEM (`S-1-5-18`), a DACL of one `ACCESS_ALLOWED` ACE
granting `GENERIC_ALL` to SYSTEM flagged
`OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE` so that inheritance
derives a child descriptor for every inode created on the mount
afterwards, and no SACL.

The writes go through the kernel-internal xattr path, bypassing both
the FACS denial hooks and the LSM setxattr permission hook. They
consult no token — at the point either runs there may be no meaningful
subject — and the seeded descriptor is the sole authority for the
mount until trusted userspace replaces it. They depend on nothing
beyond the LSM scaffold that allocates inode and superblock blobs.

These are not exempt from later management: trusted userspace can
overwrite the per-inode descriptor or change the superblock's class
once it holds the privileges.

## 3.9.5.5 Boot artifacts

A filesystem shipped as a boot artifact — a squashfs concatenated into
the initrd, a vendor squashfs delivered as a package, a flashed
partition image — defaults to `facs_deny_missing` and the kernel does
**not** seed it. These are already-populated trees the kernel cannot
extend at mount time, and in the read-only case cannot extend at all.

The obligation falls on the build pipeline, which emits
`security.peios.sd` on every inode ordinary access checks will reach.
`mksquashfs`, the ext utilities and the standard userland xattr
surfaces all preserve `security.*` natively.

An artifact without descriptors is a packaging defect. FACS treats
every missing descriptor on a `facs_deny_missing` mount as a
corruption indicator and denies. The operator path is to rebuild the
artifact, or to adopt the superblock under a synthesise class.

## 3.9.5.6 Administration

Trusted userspace adopts a mounted filesystem by calling
`kacs_set_mount_policy` on a descriptor naming any object on the
target superblock; `O_PATH` descriptors are valid targets. The change
applies to the superblock, not the pathname used to reach it.

The call requires enabled `SeTcbPrivilege` and marks it used. The
public ABI accepts only the three managed classes; `unmanaged`,
unknown values, nonzero reserved flags and malformed arguments all
fail closed.

The optional template is accepted only with a synthesise class. It is
a complete self-relative descriptor rather than a subset, passes
structural validation, and is at most 65535 bytes. A null pointer with
zero length clears it. Setting `facs_deny_missing` clears it and
rejects non-empty template input. Pointer and length mismatches and
invalid bytes fail before any state changes.

Policy changes are **lazy**. They do not walk the filesystem and do
not stamp anything. The superblock carries a monotonic generation
counter, incremented on every successful policy or template
replacement. Missing-descriptor, ephemeral-synthetic and
not-yet-written-back persistent-synthetic cache entries record the
generation they came from and are discarded and repopulated when it
changes. Xattr-backed and corrupt-descriptor caches are not made valid
by a policy change, and open file descriptions keep their immutable
masks.

## 3.9.5.7 Missing descriptors

Under **`facs_deny_missing`**, no descriptor means deny. Two
exceptions keep the repair path open. `SeChangeNotifyPrivilege`
bypasses intermediate traverse checks including on directories with no
descriptor, though not explicit `chdir()`, `chroot()` or `fchdir()`
use-time checks. And `O_PATH` opens bypass the open hook entirely, so
a file with a missing descriptor can still be acquired as an `O_PATH`
reference — which is exactly the repair route: `open(path, O_PATH)`
then `kacs_set_sd` with `AT_EMPTY_PATH` under `SeRestorePrivilege`.

Under the **synthesise classes**, a missing descriptor is generated
from two sources in order. First, **inheritance from the parent**: if
the parent has one, the inheritance algorithm runs as though a new
file were being created. Otherwise the **mount-level template**,
applied where there is no parent descriptor — typically only at the
mount root. With no template configured, the fallback grants
`GENERIC_ALL` to SYSTEM and `BUILTIN\Administrators` and
`GENERIC_READ | GENERIC_EXECUTE` to Everyone, owned by SYSTEM with
SYSTEM as group.

Because these files already exist, the accessor is not their creator.
Where inheritance needs creator inputs — owner, primary group, default
DACL — a synthetic system-policy creator supplies them: the template's
owner, group and DACL if one exists, the fallback's otherwise. **The
accessor's token never affects the synthesised descriptor**, and the
synthesis path takes no subject token at all.

Inheritance is recursive — a parent whose own descriptor is missing is
synthesised first, walking toward the mount root where the template
terminates it. The walk is bounded at 32 ancestor levels; a target
nested deeper than that below the nearest resolvable ancestor fails
closed with `EACCES` rather than synthesising.

An **ephemeral** synthesis is cached in the inode blob only and never
written back, leaving the original filesystem unmodified. A
**persistent** one is additionally written to the xattr so the medium
acquires durable descriptors — but never inline.

## 3.9.5.8 Deferred write-back

Synthesis runs holding the FACS inode lock, and writing the xattr
takes the inode's `i_rwsem`. Doing that inline would acquire `i_rwsem`
under the FACS lock, inverting the order the access path requires, and
would self-deadlock when synthesis is reached from a metadata
operation whose VFS caller already holds `i_rwsem`. Write-back
therefore runs with no FACS or VFS lock held.

Synthesis caches the descriptor immediately and marks the entry
pending. The access decision is correct from that cached value the
instant synthesis completes — correctness never depends on the xattr
reaching disk. The write-back runs later from a task-work callback
firing as the triggering syscall returns to userspace, so a persistent
descriptor is normally on disk by the time the operation that first
observed it missing returns.

A pending entry is generation-tagged exactly like an ephemeral one, so
a policy or template change before the write-back discards it and
re-synthesises; a stale pre-change descriptor is never pinned to disk.

Write-back is best-effort, and can be because the synthesised
descriptor is a deterministic function of the parent or the template —
the on-disk xattr is a cache of a recomputable value, not unique
state. If it does not happen, because the entry was evicted or the
task exited first, the identical descriptor is re-synthesised on next
access and retried. A failed or skipped write-back never fails the
operation that triggered synthesis. Kernel threads, and a failure to
queue the callback, fall back to re-synthesis the same way.

Once written, the next cache miss reads it back as an ordinary
xattr-backed descriptor: durable, no longer generation-tagged, and
never synthesised again.

An ancestor synthesised only to supply inheritance inputs for a
descendant is itself pending, and persists under the same rules when
it is next accessed in its own right.

## 3.9.5.9 Corrupt descriptors

A descriptor xattr that exists but fails structural validation is
corrupt, and the policy is fail-closed: deny all access, do not call
AccessCheck, and never treat a truncated DACL as an empty one.

Every encounter emits an audit event, fired exactly once per inode per
cache population rather than per access, so a hot corrupt inode does
not flood the log.

Recovery is a process holding `SeRestorePrivilege` calling
set-security to overwrite it. Offline repair tools can also rewrite
xattrs directly on an unmounted filesystem.

## 3.9.5.10 NFS client mounts

NFS is the one managed class where the sole-authority guarantee does
not hold. The server enforces its own access control independently:
FACS evaluates locally against a synthesised descriptor, and the
server may deny I/O that FACS allowed. A locally authorized `open()`
can therefore produce a descriptor whose `read()` calls fail. This is
inherent to network filesystems with server-side enforcement, and
nothing suppresses the server's denial.

---

# 3.9.6 The Set-Security Interface

_Peios / Advanced Peios / PKM / KACS / FACS_

> The single syscall all descriptor modification flows through — ownership, integrity labels, the SeRestorePrivilege bypass and mandatory attributes.

All descriptor modification flows through one syscall. The caller
provides a file descriptor or path, a bitmask naming which components
to modify, and a self-relative descriptor blob carrying the new
values.

| Flag | Component | Required right |
|---|---|---|
| `OWNER_SECURITY_INFORMATION` | Owner SID | `WRITE_OWNER` |
| `GROUP_SECURITY_INFORMATION` | Group SID | `WRITE_OWNER` |
| `DACL_SECURITY_INFORMATION` | Discretionary ACL | `WRITE_DAC` |
| `SACL_SECURITY_INFORMATION` | System ACL | `ACCESS_SYSTEM_SECURITY` |
| `LABEL_SECURITY_INFORMATION` | Mandatory integrity label | `WRITE_OWNER`, plus the integrity constraints below |

The blob is validated structurally — parseable, well-formed ACEs,
valid SIDs, at most 65535 bytes — and then only the indicated
components are merged into the existing descriptor. Unindicated
components are preserved unchanged.

The input is always one self-relative descriptor subset, never a raw
SID or ACL fragment. `SACL_SECURITY_INFORMATION` and
`LABEL_SECURITY_INFORMATION` cannot be combined in one call, because
both target the SACL field with incompatible meanings; the pair fails
with `EINVAL`.

A `SACL_SECURITY_INFORMATION` write replaces the object's **entire**
SACL. A `LABEL_SECURITY_INFORMATION` write interprets the input SACL
as the label subset only: no SACL component removes the explicit
mandatory label and returns the object to the default unlabelled
state; a present SACL contains exactly one non-inherit-only
`SYSTEM_MANDATORY_LABEL_ACE` and nothing else; and the object's
non-label SACL ACEs are preserved.

After merging, the result still has a non-null owner — the group SID
may be null — and a merge that would leave no owner fails.

MIC and PIP apply to these checks. A low-integrity caller cannot
modify a high-integrity file's descriptor even where the DACL grants
`WRITE_OWNER`.

## 3.9.6.1 Ownership

A new owner may be set only to the caller's own SID, or to a group SID
on the token carrying `SE_GROUP_OWNER`. `SeTakeOwnershipPrivilege`
allows setting ownership to the caller's own SID regardless of what
the current descriptor says, and `SeRestorePrivilege` allows any
arbitrary SID.

## 3.9.6.2 Integrity labels

Without `SeRelabelPrivilege` a caller may set a label only at or below
its own integrity level; with it, any level.

The constraint applies through **both** paths — the dedicated label
subset, and a label ACE embedded in a full SACL write. A SACL write
whose ACL contains a mandatory label ACE raising integrity above the
caller's level requires `SeRelabelPrivilege` exactly as the label path
does, even though the SACL component itself is gated only by
`ACCESS_SYSTEM_SECURITY`.

## 3.9.6.3 The SeRestorePrivilege bypass

`SeRestorePrivilege` fires inside the AccessCheck pipeline, so it
bypasses the check only where `kacs_set_sd` runs a **live** one: an
`O_PATH` descriptor with `AT_EMPTY_PATH`, a pidfd, a token descriptor
with `AT_EMPTY_PATH`, or a path. On those paths it grants every
requested right, `WRITE_OWNER`, `WRITE_DAC` and
`ACCESS_SYSTEM_SECURITY` included.

Called on an ordinary file descriptor the required rights are checked
against the cached mask instead, no AccessCheck runs, and the
privilege has no effect at all. A caller needing the bypass has to use
the `O_PATH` route — which is the mechanism behind backup restoration,
administrative repair, and the missing-descriptor repair path
(§3.9.5).

## 3.9.6.4 Mandatory resource attributes

When a caller modifies the SACL, the existing and new SACLs are
compared for changes to `SYSTEM_RESOURCE_ATTRIBUTE_ACE` entries. An
existing attribute carrying `CLAIM_SECURITY_ATTRIBUTE_MANDATORY`
(0x0020) cannot be removed, nor its values modified, without
`SeTcbPrivilege` — and an attempt without it fails the entire call
rather than silently dropping the change.

## 3.9.6.5 Write mechanics

The updated descriptor is serialised to self-relative binary form and
written to the xattr through an internal kernel path bypassing the
denial hook, and the in-memory cache is updated. An audit event is
emitted if the file's SACL carries a matching audit ACE.

The cache update is not atomic with the xattr write; §3.9.5 describes
the lock ordering that forces this and the last-writer-wins window it
produces.

---

# 3.9.7 The StrataFS Copy-Up Context

_Peios / Advanced Peios / PKM / KACS / FACS_

> Copy-up is the internal realisation of an already-authorized operation — its admission and lifetime, object and phase binding, and exempt operations.

StrataFS copy-up is the internal realisation of an operation already
authorized against a StrataFS handle — not a second caller-requested
operation. KACS therefore provides a kernel-internal context so that
the mechanics of copy-up introduce no new rights checks against the
task that happens to execute them.

The context exempts **KACS caller authorization only**. It does not
replace credentials, borrow an identity, grant a privilege, bypass
another LSM, neutralise an underlying filesystem check, make a
read-only mount writable, or suppress immutable, append-only, quota,
space, I/O or format errors. Every exemption is a return before the
authorize call, and every mutation still goes through the ordinary
`vfs_*` path under `mnt_want_write()`.

## 3.9.7.1 Admission and lifetime

Only the in-kernel StrataFS implementation can create or enter a
context. There is no userspace surface of any kind — no ABI, file
descriptor, token, ioctl, syscall or securityfs control — and the
copy-up API is declared in a kernel-private header with no exported
symbols.

A context is created only after the StrataFS operation requiring
copy-up has passed its complete outer authorization. KACS does not
verify that: the context creation call performs no check, and the
ordering is satisfied by StrataFS calling in the right order. The
context is not itself an alternative authorization path.

The exemption covers namespace creation the outer handle authorises
without an add-entry right on the create stratum, so the context is
reachable only from a create-enabled mount established by a caller
holding `CAP_SYS_ADMIN` in the **initial** user namespace. That
closure rests on an explicit test of the mounter's user namespace at
stack establishment — the capability half contributes nothing to it,
because the KACS switchboard discards the target namespace and answers
from `SeTcbPrivilege` alone (§3.10.2). The check is made when the
immutable stack is established rather than when a copy-up runs, so
descriptor delegation cannot reintroduce acting-task authorization.
Reconfiguration cannot re-supply the strata list.

A context attaches to at most one task, and a task carries at most
one. It is not inherited by `fork()`, `clone()` or `execve()` — exec
explicitly clears it. A refcounted context can be transferred to a
kernel worker, but the originating task leaves it first. Leaving,
task exit, every error path, and completion all remove the attachment
and clear any armed phase.

The interface fails closed on nesting, concurrent attachment, a stale
phase, or an object mismatch. A mismatched operation is evaluated
normally and neither consumes nor broadens the armed exemption.

## 3.9.7.2 Object and phase binding

Creation pins the exact provider path, its current inode, a complete
validated copy of its effective descriptor, and the provider-visible
`security.capability` value or its absence. Every later positive path
is paired with a pinned inode as well — retaining a dentry alone is
insufficient, because an unlink followed by recreation can
reinstantiate it over a different inode.

One phase is armed at a time:

| Phase | Objects admitted |
|---|---|
| Source read | The pinned provider only. |
| Create | One pinned provider, one destination parent, and either one named negative dentry or one anonymous creation in that parent. Used for both parent materialisation and the staged object. |
| Populate | The pinned provider and the one staged object bound to the context. |
| Publish by link | The bound staged object, one destination parent, one absent destination dentry. |
| Publish by rename | The bound staged object and its parent, one destination parent, one absent destination dentry. |
| Cleanup | One bound staging or materialisation dentry and its exact parent. |
| Orphan cleanup | One positive provider dentry carrying an authenticated stale staging marker, and its exact parent. |
| Orphan-marker cleanup | The exact positive provider dentry whose authenticated stale marker is being removed after publication. |

Path, dentry, inode, parent, object type and operation kind are all
compared wherever the hook supplies them — and path comparison
includes the mount, so the same dentry reached through a different
mount of the same superblock does not match. An inode-only hook can
match the pinned inode, but that does not authorize a pathname
operation on another dentry, and a phase never acts as a wildcard for
other objects of the same filesystem or directory.

The staged binding stays valid only while its dentry names the pinned
inode under the pinned staging parent, and that parent still names its
own pinned inode. A rename or parent substitution invalidates the
populate, protected-metadata and publish exemptions even when the
staged dentry and inode are themselves unchanged.

Named creation is bound to the exact final component. Anonymous
creation is bound to its parent, expected object type, the attached
task, and the single armed create phase — and once created, the
anonymous object has to be bound as the staged object before populate,
publish or cleanup can use it.

Where the destination is a stacking filesystem creating a real inode
below the armed destination dentry, the transition is authenticated at
the exact outer dentry, and only the one subsequent real-inode
security initialisation of the expected type receives the pinned
provider descriptor. The real inode is not the staged binding: the
outer inode is separately anchored, through the matching post-create
event for an anonymous object or an explicit confirmation of the exact
positive outer dentry for a named one. A missing, repeated, mismatched
or out-of-order transition fails closed, and an inner post-create
event from the real filesystem is rejected as the outer anchor by
superblock comparison.

KACS retains the exact path, inode, parent path and parent inode of
every named object whose creation it admits. Cleanup can be armed only
for one of those or for the exact bound staging object; an unrelated
positive dentry fails with `ESTALE`. The cleanup setup call is not
itself authority to choose a deletion victim.

After an atomic rename or link publishes the staged inode, the staging
identity can be rebound to the published path, but only while mount,
inode, parent dentry and pinned parent inode all still match. The
rebind API exists and is **not called** by StrataFS: the link case is
rebound internally by the publish path, and the rename case relies on
publish preserving the dentry. That holds because publication always
renames within a single parent. A cross-directory publish rename would
silently invalidate the staging binding, since the rename-publish
entry point does not require the destination parent to equal the
staging parent — currently unreachable, but the guard is the caller's
discipline rather than the interface's.

Named staging entries carry `security.peios.stratafs_staging`.
Caller-originated writes and removals of it are denied. A write or
removal is admitted only on the exact bound staging inode during
populate, or on the exact orphan-marker object during authenticated
recovery — where the predicate admits a *write* as well as a removal,
being shared between the setxattr and removexattr hooks. Probing the
marker for recovery is a kernel-only raw read conveying no caller
authority, and orphan deletion is bound to the exact dentry, inode,
parent dentry and parent inode supplied when the phase was armed, so
an arbitrary name sharing the staging prefix never matches. Recovery
processes at most 128 entries per batch.

## 3.9.7.3 Exempt operations

For a matching object in a matching phase, the ordinary AccessCheck,
cached-grant check, privilege check and caller-access audit decision
are omitted at these points:

| Copy-up action | Enforcement points |
|---|---|
| Open and read provider data or a symlink target | `security_inode_permission`, `security_file_open`, `security_file_permission`, `security_inode_readlink` |
| Read provider attributes and extended attributes | the inode and file getattr, setattr-preflight, getxattr and listxattr paths as applicable |
| Create a staged file, directory or symlink, and materialise parents | `security_inode_permission`, `security_inode_create`, `security_inode_mkdir`, `security_inode_symlink`, `security_inode_init_security`; for a stacking destination also `security_dentry_create_files_as` and, for an anonymous object, `security_inode_post_create_tmpfile` |
| Populate staged data, attributes and eligible non-descriptor xattrs | `security_inode_permission`, `security_file_open`, `security_file_permission`, the inode and file setattr paths, setxattr and removexattr. `security.capability` uses only the dedicated clone call below. |
| Publish | `security_inode_permission` on the exact staged object or destination parent, then `security_inode_link` or `security_inode_rename` |
| Remove staging or roll back materialised entries | `security_inode_permission` on the exact parent, then `security_inode_unlink` or `security_inode_rmdir` |

A `security_inode_permission` or `security_file_permission` match also
matches the requested mask. Provider access is
read-only — write, append and non-directory execute requests never
match it. Staging access is limited to the read, write and append
masks population needs, plus execute and chdir for directories, and
namespace parents to the write and traverse masks the one armed action
needs, plus open and chdir. Unknown mask bits fail closed.

The list is exhaustive. The context does not exempt execution, memory
mapping, ioctl, locking, arbitrary `fcntl`, device access, process
access, socket access, mount operations, or operations on a descriptor
installed into a userspace file table — each verified by the absence
of any copy-up branch on those paths. Internal copy-up files are
additionally denied `statfs`, `truncate`, `fsync` and `fallocate`.

A file opened internally under the exemption is marked
copy-up-internal in its blob and carries a granted mask of zero. It is
usable without a cached caller grant only while the same context is
attached and its phase admits that exact file. Use after phase
completion, from another task, or after transfer through `SCM_RIGHTS`
fails closed. Each armed phase has a distinct monotonically increasing
generation — overflowing it fails closed — and an internal file is
sealed to the generation it was opened in, so a later phase of the
same kind after a worker transfer cannot reactivate an older file.
Final release drops the context reference.

The one exception is the read-only provider-directory cursor used by
bounded staging recovery. StrataFS may retain that file while it
leaves the context to clean one captured batch, then resume it after
re-entering the same context and arming a new source-read phase. Only
a directory file already marked internal for that exact context, still
naming the pinned provider path and inode, opened for read without
write, execute or path-only mode, and inside that attached
source-read phase, resumes; it is then resealed to the new generation.
Between phases it is unusable and conveys no deletion authority.

## 3.9.7.4 Backing-file adoption

Once a regular-file copy is published, one exact copy-up-internal
backing file can become the backing file for the outer StrataFS open
description. The staging binding is verified, and the backing file's
recorded user path is confirmed to be that same outer description;
the outer description's immutable granted and continuous-audit
snapshot is then copied across and the internal marker removed. This
is a transfer of authority already attached to the descriptor, not a
new AccessCheck against the task that caused the copy-up, which is
what preserves descriptor delegation when that task is not the opener.
It is one-shot, and requires the backing file mode.

The ordering is worth noting: StrataFS performs the adoption **before**
publishing the anonymous staged object rather than after, so what is
verified is the still-bound staging binding rather than a published
one.

## 3.9.7.5 Deferred deletion

Delete-on-close is authorized when armed and recorded on the open file
description (§3.9.2). At final close there is no re-authorization
against the closing task. Instead a synchronous, non-nesting internal
deletion scope is armed for that exact outer file, and StrataFS binds
it once to the exact provider parent, dentry and inode selected from
the descriptor's settled provider. Only the corresponding outer and
lower unlink calls match, and the scope is cleared on every return.

The mechanism never turns an ordinary close into deletion authority,
never admits a different provider entry, and never permits unlinking
an entry that no longer names the descriptor's inode. If the original
entry has disappeared or changed identity, the deletion is already
complete and no exemption is used.

## 3.9.7.6 Exact protected-metadata cloning

KACS owns descriptor cloning rather than StrataFS raw-xattr code.
Before a create phase is armed, the provider's complete effective
descriptor is resolved and pinned using the ordinary mount-policy and
corrupt-descriptor rules (§3.9.5). A missing, corrupt, unresolvable,
oversized or unsupported descriptor fails the phase before the
destination is created.

For a matching inode security initialisation, an exact byte-for-byte
copy of the pinned descriptor is installed instead of inheritance
running. The canonical xattr is installed as part of inode creation
and the inode's validated parsed cache is seeded from identical bytes
before the inode becomes usable. Failure to allocate, validate or
install either representation fails creation — there is no window in
which a named staging inode carries an inherited or otherwise weaker
descriptor.

On a stacking destination the same applies to the real inode created
below the outer dentry, with the authenticated outer transition as the
only authority to redirect installation there. The pinned bytes are
installed and cached during the real inode's creation, before the
outer object is confirmed or bound; inheriting the parent's descriptor
and repairing it afterwards would not be conforming.

The canonical descriptor is not copied by ordinary xattr enumeration —
it is reported as cancelled so a stacking filesystem discards it — and
raw canonical getxattr and setxattr stay denied even inside the
context, with the hook-side denial evaluated before any phase match.
The context does not override the unconditional denial of POSIX ACL
mutation either.

Raw setxattr, including through an internal copy-up file, remains
unable to install `security.capability`. Where the provider has one,
StrataFS calls the dedicated clone entry point during populate, after
any operation that might clear file capabilities. That call accepts
only a kernel buffer exactly matching the pinned value, under the same
user namespace it was pinned in, for the still-bound staged inode. It
re-reads the provider immediately before installing and fails with
`ESTALE` if the value or its presence changed.

The call copies the caller's buffer before comparing, and installs
with `XATTR_CREATE` through the ordinary VFS path, so mount-idmap
conversion, xattr validation, filesystem permission and format checks
and other LSM checks all still apply. The otherwise-dead
`CAP_SETFCAP` gate is satisfied only synchronously inside that
validated call, and the corresponding setxattr re-entry is admitted
only after the first hook matches the exact staged inode. The
capability answer targets only the pinned caller namespace or the
staged inode's filesystem namespace, and the condition is cleared on
every return. Nothing is installed when the provider had no attribute,
a different one, or an unreadable or invalid one. The exception does
not revive exec-time file-capability grants.

There is one asymmetry here: the removal path does not reject
`security.capability` the way the set path does, so an internal
copy-up descriptor can remove it from the staged object during
populate.

Other eligible provider xattrs follow StrataFS's replication rules
while KACS's caller checks on their source and staging objects are
exempted as above. An ineligible xattr that cannot be replicated
triggers StrataFS's ordinary copy-up failure rule rather than being
silently omitted.

## 3.9.7.7 Auditing

The outer authorized handle operation remains subject to ordinary
audit. No second caller AccessCheck or privilege-use audit is emitted
for an exempt internal sub-operation, since that would attribute
StrataFS mechanics to a caller decision that never happened — and
internal files carry a continuous-audit mask of zero, so they generate
no per-operation events either. The `CAP_SETFCAP` satisfaction path
returns before the capability check that would record privilege use.

StrataFS decides when its own copy-up lifecycle and failure events
occur; KACS supplies the kernel-only emitter, so KMES stamps each
event with the effective token of the task whose operation caused it
(§2.2). Two of those emissions are best-effort: an allocation failure,
or an operation string that is empty or over 64 bytes, drops the event
silently.

Denied mismatches, and operations performed with no active matching
context, follow the ordinary authorization and audit paths.

---

# 3.10.1 Credential Projection

_Peios / Advanced Peios / PKM / KACS / The Linux Credential Model_

> Linux applications call getuid() and know nothing of tokens — how KACS projects a token onto Linux credentials, one way only.

KACS tokens are the sole identity-based authorization mechanism, and
Linux applications do not know tokens exist. They call `getuid()`,
`getgid()` and `getgroups()`, read `/proc/self/status`, and assume
those numbers determine their access. KACS projects token identity
onto standard Linux credentials so unmodified applications work.

When a token is installed on a process, the process's Linux
credentials are set to match. The numbers themselves are **already on
the token**: the user SID's projected uid, the primary group SID's
projected gid, and a projected supplementary gid per group SID are all
computed by authd when the token is minted, and KACS copies them.

**KACS never resolves a SID to a number itself.** It holds no directory
handle and consults nothing at install time — which is what makes
projection cheap enough to do on every credential change, and what
keeps a name-service outage from being able to change what a running
process may do.

How a SID becomes a number is therefore not KACS's rule to state, and
this chapter deliberately does not restate it. The authority is the
principal source interface's numeric scope (PSPU §2): identifiers are
**computed** — an authority grants a source a band `(base, count)` and
derives `base + r` from a relative identifier — rather than looked up
per principal, and a source's assertion outside its band is refused
rather than clamped.

`65534` does appear in KACS, but not as a "no attribute was set"
fallback: it is `ANONYMOUS_PROJECTED_ID`, what the projected-id
accessors return for the anonymous identity and for an invalid token
pointer. It is a sentinel for *no identity*, not a default for an
identity whose number could not be found.

The consequences are mostly convenient ones. No process runs as UID 0
unless it holds the SYSTEM token — enforced, not merely expected: a
token creation naming a projected UID of 0 with any user SID other
than `S-1-5-18` is rejected, and the projection path refuses it again
at install time. Home directories work naturally, because
`getpwuid(getuid())` returns the right answer when the UID is real and
consistent with NSS. And different services get different UIDs, which
is incidental defence in depth alongside KACS's own enforcement.

## 3.10.1.1 Projection is one-way

Token state flows into credential fields and never the reverse. The
projected credentials are observational compatibility data; the token
is the authority. The `setuid` family restores the old credential
rather than deriving a token from it (§3.10.3).

Projection reflects **all** groups regardless of enabled state, so
adjusting groups never triggers recalculation.

Projected credentials reflect the **effective** token — the
impersonated one during impersonation, the primary one otherwise. When
a service thread impersonates a client and creates a file, the file is
owned by the client's projected UID, quota is charged to the client,
and audit attributes to the client.

The two accessors deliberately disagree during impersonation.
`current_fsuid()` reads the projected UID from the *effective*
credential, so it yields the client's UID; `getuid()` reads the
*primary* credential's UID, so it yields the service's. During
impersonation `getuid()` returns the service and `current_fsuid()`
returns the client, and that is the intended behaviour rather than an
inconsistency.

One caveat applies to a credential carrying no token at all — a blank
credential, or one created before KACS initialised. `current_fsuid()`
falls back to `cred->fsuid` in that case, and the capability
switchboard denies before consulting the ALLOW list (§3.10.2), so
Linux DAC becomes authoritative for such a task.

## 3.10.1.2 Precomputed values

Every token carries precomputed projected UID and GID values,
calculated by authd at creation and stored on the token. KACS never
resolves a SID-to-UID mapping at runtime; the accessors are pure field
reads.

Because SIDs are one namespace while Linux UIDs and GIDs are two,
authd allocates from a **single unified counter** across all principal
types rather than a separate one per namespace, so every SID projects
to a unique number whichever Linux namespace it lands in. A user and a
group can never collide on a number, which is what lets one SID answer
both `getuid()` and `getgid()` questions without ambiguity.

---

# 3.10.2 DAC Neutralisation

_Peios / Advanced Peios / PKM / KACS / The Linux Credential Model_

> Linux evaluates DAC before any LSM hook fires, so KACS has to neutralise it — the capability switchboard, the compatibility state and the LSM stack.

Linux evaluates DAC — UID, GID and mode-bit checks — *before*
consulting LSM hooks. If DAC denies an operation the hook never fires,
and KACS cannot override a DAC denial: LSM hooks are restrictive, able
to further deny what DAC would allow but never to grant what DAC has
refused.

KACS therefore neutralises DAC so the hooks always fire. Every process
receives a set of Linux capabilities that bypass the DAC gates, and
those capabilities are mandatory substrate rather than grants.

## 3.10.2.1 The capability switchboard

Linux's capabilities fall into three categories, and
`security_capable()` is authoritative for all of them — the raw
capability sets on `struct cred` are compatibility-visible state that
answers nothing.

**ALLOW** capabilities exist to override UID-based permission checks
on operations KACS enforces through its own hooks, so DAC never blocks
something KACS will evaluate independently:

| CAP | Name | Rationale |
|---:|---|---|
| 0 | `CAP_CHOWN` | KACS file hooks enforce. |
| 1 | `CAP_DAC_OVERRIDE` | KACS file hooks enforce. |
| 2 | `CAP_DAC_READ_SEARCH` | KACS file hooks enforce. |
| 3 | `CAP_FOWNER` | KACS file hooks enforce. |
| 4 | `CAP_FSETID` | KACS file hooks enforce. |
| 5 | `CAP_KILL` | The `task_kill` hook enforces. |
| 6 | `CAP_SETGID` | Cosmetic under KACS. |
| 7 | `CAP_SETUID` | Cosmetic under KACS. |
| 11 | `CAP_NET_BROADCAST` | Unused in modern kernels. |
| 15 | `CAP_IPC_OWNER` | KACS IPC hooks enforce. |
| 28 | `CAP_LEASE` | KACS file hooks enforce. |

**PRIVILEGE** capabilities gate operations no other KACS hook covers,
and map to a KACS privilege:

| CAP | Name | Privilege |
|---:|---|---|
| 9 | `CAP_LINUX_IMMUTABLE` | `SeTcbPrivilege` |
| 10 | `CAP_NET_BIND_SERVICE` | `SeBindPrivilegedPortPrivilege` |
| 12 | `CAP_NET_ADMIN` | `SeTcbPrivilege` |
| 13 | `CAP_NET_RAW` | `SeTcbPrivilege` |
| 14 | `CAP_IPC_LOCK` | `SeLockMemoryPrivilege` |
| 16 | `CAP_SYS_MODULE` | `SeLoadDriverPrivilege` |
| 17 | `CAP_SYS_RAWIO` | `SeTcbPrivilege` |
| 18 | `CAP_SYS_CHROOT` | `SeTcbPrivilege` |
| 19 | `CAP_SYS_PTRACE` | `SeDebugPrivilege` |
| 20 | `CAP_SYS_PACCT` | `SeTcbPrivilege` |
| 21 | `CAP_SYS_ADMIN` | `SeTcbPrivilege` (but see the mount note below) |
| 22 | `CAP_SYS_BOOT` | `SeShutdownPrivilege` |
| 23 | `CAP_SYS_NICE` | `SeIncreaseBasePriorityPrivilege` |
| 24 | `CAP_SYS_RESOURCE` | `SeIncreaseQuotaPrivilege` |
| 25 | `CAP_SYS_TIME` | `SeSystemtimePrivilege` |
| 26 | `CAP_SYS_TTY_CONFIG` | `SeTcbPrivilege` |
| 27 | `CAP_MKNOD` | `SeTcbPrivilege` |
| 29 | `CAP_AUDIT_WRITE` | `SeAuditPrivilege` |
| 30 | `CAP_AUDIT_CONTROL` | `SeSecurityPrivilege` |
| 33 | `CAP_MAC_ADMIN` | `SeSecurityPrivilege` |
| 34 | `CAP_SYSLOG` | `SeTcbPrivilege` |
| 35 | `CAP_WAKE_ALARM` | `SeTcbPrivilege` |
| 36 | `CAP_BLOCK_SUSPEND` | `SeTcbPrivilege` |
| 37 | `CAP_AUDIT_READ` | `SeSecurityPrivilege` |
| 38 | `CAP_PERFMON` | `SeSystemProfilePrivilege` **or** `SeProfileSingleProcessPrivilege` **or** `SeLoadDriverPrivilege` |
| 39 | `CAP_BPF` | `SeTcbPrivilege` |
| 40 | `CAP_CHECKPOINT_RESTORE` | `SeTcbPrivilege` |

`CAP_PERFMON` is the one **OR-mapped** entry: the Linux capability
genuinely spans several Peios privilege tiers, and no single privilege
covers everything it gates. The check succeeds if the caller holds any
of the three, and every one it holds is marked used. Per-operation
enforcement then happens at the relevant syscall hook, which checks
the *specific* privilege the *specific* operation needs (§3.7).
OR-mapping stops the capability ceiling manufacturing false denials;
it grants nothing the holder does not already have.

`CAP_SYS_ADMIN` maps to `SeTcbPrivilege` and is **not** OR-mapped, which
would be the obvious way to let administrators mount and is the wrong
one: `CAP_SYS_ADMIN` gates dozens of unrelated operations, so widening
it would hand out far more than mounting.

Mounting is handled outside the capability table instead. `may_mount()`
(`fs/namespace.c`, patched) calls `pkm_kacs_may_manage_volumes()`, which
accepts `SeManageVolumePrivilege` **or** `SeTcbPrivilege`, before falling
back to the ordinary `CAP_SYS_ADMIN` check. Every other `CAP_SYS_ADMIN`
caller still needs the TCB.

It has to be asked there rather than through the `sb_mount` LSM hook:
`may_mount()`'s capability check runs *before* `security_sb_mount()`, so
an LSM is never consulted about a mount the capability check already
refused. A hook can narrow that decision; it cannot widen it.

`CAP_SYS_BOOT` carries an extra condition the table cannot express: a
token whose logon session is of a remote origin — Network,
NetworkCleartext or NewCredentials — additionally requires
`SeRemoteShutdownPrivilege`.

**DENY** capabilities are refused unconditionally, whatever privilege
the caller holds: `CAP_SETPCAP` (8) and `CAP_SETFCAP` (31), because
capabilities are dead under KACS, and `CAP_MAC_OVERRIDE` (32), because
KACS is the active LSM and must not be bypassable.

An unmapped or unknown capability is denied by default. The
switchboard fails closed.

## 3.10.2.2 Compatibility state

Programs may inspect the credential capability sets with `capget()` or
`/proc/<pid>/status`, and may attempt to mutate the non-ALLOW subset
with `capset()` or `prctl()`. None of that is authoritative.

`capget()` and `/proc/<pid>/status` report the ALLOW substrate as
present in the effective, permitted and inheritable sets — reported as
`CapEff`, `CapPrm` and `CapInh`, and additionally `CapBnd`, by the proc
interface. Non-ALLOW bits present in the
credential state may also be reported, but grant no authority.
`CapAmb` reports raw Linux ambient state; the ALLOW substrate does not
depend on ambient capabilities.

The strict invariant is that the ALLOW set is mandatory substrate and
has to survive wherever Linux capability mechanics would otherwise
drop it out from under KACS. `capset()` rejects any request clearing
an ALLOW capability from the effective, permitted or inheritable sets,
and bounding-set drops and ambient manipulation reject anything that
would clear or exclude one. The implementation is slightly stricter
than that: an ambient *raise* of an ALLOW capability is refused too,
not only a clear.

After that validation, `capset()` follows ordinary Linux ambient
behaviour — ambient bits no longer present in both the requested
permitted and inheritable sets may be cleared. Because requests
clearing ALLOW bits from permitted or inheritable are denied outright,
that intersection can never indirectly clear an existing ALLOW ambient
bit.

Direct mutation of non-ALLOW state is therefore compatibility-only. It
may change what `capget()` reports and cannot change the authority
answer for any capability-gated operation.

## 3.10.2.3 Neutralised native paths

Native commoncap helpers that make raw capability-subset decisions
*before* a KACS hook are neutralised where KACS has an authoritative
hook of its own. That covers the subset gates in ptrace access,
`PTRACE_TRACEME`, and `task_setnice`, `task_setscheduler` and
`task_setioprio` — each replaced by an unconditional allow, leaving
the KACS process-descriptor and PIP hooks to decide. Capability checks
that reach `security_capable()` directly stay under the switchboard.

For raw xattr operations the FACS metadata hooks are authoritative, so
the native security-xattr capability prechecks that would run first
are skipped. This does not revive Linux file capabilities: installing
or replacing non-empty `security.capability` data stays denied by the
dead `CAP_SETFCAP` policy, with the single exception of the
KACS-owned StrataFS clone (§3.9.7), and exec-time file-capability
grants remain suppressed. Removing stale metadata goes through the
ordinary `FILE_WRITE_EA` path.

One structural wrinkle: `security_capable()` reaches the KACS
switchboard twice — once through the patched commoncap entry point and
once through the KACS `capable` hook — so a privilege consulted this
way is marked used twice. The authorization answer is unaffected; the
privilege-use accounting double-counts.

## 3.10.2.4 The LSM stack

MAC LSMs — SELinux, AppArmor, SMACK, TOMOYO — and the BPF LSM have to
be disabled. They would independently deny operations from their own
label and policy systems, undermining KACS's claim to be the sole
identity-based authorization mechanism and FACS's to be the sole file
access authority. Non-MAC LSMs are permitted: landlock, lockdown, yama
and integrity make no identity-based access decisions and stack
safely.

The check is made at initialisation and KACS refuses to activate if it
fails — but it is a **build-configuration** test rather than an
inspection of the live LSM stack. It tests whether each conflicting
LSM is enabled in the kernel config, and never parses `CONFIG_LSM` or
enumerates what is actually registered.

---

# 3.10.3 setuid Behaviour

_Peios / Advanced Peios / PKM / KACS / The Linux Credential Model_

> Under KACS a UID has no security properties — what the setuid syscalls do, what the setuid bit does at exec, and the compatibility gaps that remain.

Under KACS a UID has no security properties whatever. It is not
consulted in any access decision, appears in no descriptor, and is
referenced by no ACE. It is a compatibility value stored in
`struct cred` for the sole purpose of answering `getuid()`.

## 3.10.3.1 The setuid syscalls

The `setuid` family — `setuid`, `setgid`, `setresuid`, `setresgid`,
`setgroups` — changes Linux credentials.

**Without `SeAssignPrimaryTokenPrivilege`**, which is the common case,
the call is a silent no-op. It returns success, and every credential
field is restored from the old credential: UIDs, GIDs, supplementary
groups, capabilities. Neither the credential nor the token changes,
and the process's authority before and after is identical.

The silent success preserves consistency between the visible UID and
the KACS identity. Changing the UID without changing the token would
produce subtle failures — the wrong home directory from `getpwuid()`,
for instance — for no security benefit.

**With `SeAssignPrimaryTokenPrivilege`** the design calls for the
credential change to trigger a full identity swap, redirected to authd
to obtain a token for the target UID's principal, so that both token
and credential change together.

That is not what happens. A caller holding the privilege receives
`EOPNOTSUPP` and the call **fails**. There is no authd redirect
anywhere in the LSM. The practical effect is that the privileged path
is unavailable rather than dangerous: a TCB component cannot change
identity this way, and has to install a token directly instead
(§3.2.3).

## 3.10.3.2 The setuid bit on exec

The filesystem setuid bit tells the kernel to change the effective UID
to the file owner's on exec.

**Without the privilege**, the Linux-visible UID and GID slots change
to the file owner's identity while the token is untouched — a cosmetic
escalation in which the process sees `geteuid() == 0` while KACS
continues to enforce the original token. Concretely `uid` and `suid`
are set from `euid`, the GID counterparts mirror that, `fsuid` and
`fsgid` are carried over unchanged from the old credential, and the
token is cloned as-is.

**With the privilege**, the design calls for the slots *and* the token
to change — genuine escalation. As with the syscall, this is not
implemented: an exec that would change UID or GID under a token
holding `SeAssignPrimaryTokenPrivilege` returns `EOPNOTSUPP` and the
exec fails.

The asymmetry between the two mechanisms is intentional in the design.
`setuid()` is de-escalation, so leaving everything unchanged is the
safe failure mode. The setuid bit is escalation, and the target binary
expects the euid and will check it — `sudo` verifies
`geteuid() == 0` — so the euid has to change for the binary to
function at all.

| Mechanism | With privilege | Without privilege |
|---|---|---|
| `setuid()` syscall | Designed: all UIDs and token change. Actual: fails with `EOPNOTSUPP`. |  Silent no-op |
| Setuid bit on exec | Designed: euid/suid and token change. Actual: exec fails with `EOPNOTSUPP`. | euid/suid change, token unchanged |

## 3.10.3.3 The current_fsuid patch

The kernel calls `current_fsuid()` whenever it needs a UID for a
filesystem operation — file creation ownership, quota tracking,
keyring lookup, NFS credentials. KACS redefines it, along with
`current_fsgid()` and `current_fsuid_fsgid()`, to return the projected
value from the effective token rather than `cred->fsuid`.

So files are created owned by the projected UID, quotas track against
it, per-user keyrings are keyed by it, and an NFS server sees the real
identity rather than UID 0.

## 3.10.3.4 Compatibility gaps

**The privilege-drop pattern.** A daemon that calls `setuid(target)`
and then checks `getuid() != target` to confirm the drop sees success
returned with the UID unchanged. This is intentional — UIDs carry no
security meaning here — and software ported to Peios should use
KACS-native token operations for privilege management instead.

**Direct capability manipulation.** Software that manipulates
capabilities with `capset()` and `capget()`, writes seccomp filters,
or inspects its own capability set may behave unexpectedly (§3.10.2).

**`setfsuid()`** is a no-op for filesystem purposes, since
`current_fsuid()` ignores `cred->fsuid`; and cosmetic setuid-bit exec
transitions do not flow into the projected values either.

**`access()` and `faccessat()`** use the effective token rather than
the real credential — the entire native credential-override machinery
those calls normally use is compiled out. The concept of a "real
identity" separate from the acting one does not exist in KACS.

**`SO_PEERCRED`** returns projected UIDs rather than token
information, and because the switchboard allows `CAP_SETUID`, cosmetic
UID forgery in `SCM_CREDENTIALS` is possible. Both are Linux
compatibility metadata, not peer-token authorities: a service needing
authoritative identity uses stream or seqpacket peer-token capture, or
explicit token descriptor passing (§3.5.3).

**Legacy `auditd`** records projected UIDs, so KACS audit through KMES
replaces Linux audit for security-relevant logging.

A `uid0` utility — running a program with `cred->uid` forced to 0 for
legacy programs that refuse to start otherwise — is described in the
design and does not exist in the tree. The kernel-side guarantee it
would rely on does hold: `current_fsuid()` ignores `cred->uid`
entirely, so even with such a utility active, filesystem operations
would use the projected UID and files would be owned by the real user.

---

# Appendix 3.A KACS ABI Reference

_Peios / Advanced Peios / PKM / KACS_

> Every KACS syscall number, structure layout, constant and enumeration, generated from the uapi headers and measured by compilation.

Every name, value, offset and size in this appendix is generated
from `pkm/uapi/pkm/` by `pkm/tools/gen-kacs-abi.py`, with struct
layouts measured by compiling a probe against the real headers.
Regenerate it whenever the ABI changes; do not edit it by hand.

The names here are the ones a program actually compiles against.
Everything about the ABI a compiler cannot measure -- token query
payload shapes, the specification spellings that differ from these
names, what is documented elsewhere, and the kernel configuration
-- is in the notes appendix, §3.D, which this generator does not
touch.

## 3.A.1 Syscall numbers

Signatures are read from the `SYSCALL_DEFINE` sites in `pkm/kacs/`.

| Number | Constant | Signature |
|---:|---|---|
| 1000 | `SYS_KACS_OPEN_SELF_TOKEN` | `kacs_open_self_token(unsigned int flags, u32 access_mask)` |
| 1001 | `SYS_KACS_OPEN_PROCESS_TOKEN` | `kacs_open_process_token(int pidfd, u32 access_mask)` |
| 1002 | `SYS_KACS_OPEN_THREAD_TOKEN` | `kacs_open_thread_token(int pidfd, int tid, u32 access_mask)` |
| 1003 | `SYS_KACS_CREATE_TOKEN` | `kacs_create_token(const void __user *spec, size_t spec_len)` |
| 1004 | `SYS_KACS_CREATE_LOGON_SESSION` | `kacs_create_logon_session(const void __user *spec, size_t spec_len)` |
| 1005 | `SYS_KACS_SET_PSB` | `kacs_set_psb(int pidfd, u32 mitigations)` |
| 1006 | `SYS_KACS_DESTROY_EMPTY_LOGON_SESSION` | `kacs_destroy_empty_logon_session(u64 auth_id)` |
| 1010 | `SYS_KACS_OPEN_PEER_TOKEN` | `kacs_open_peer_token(int sock_fd)` |
| 1011 | `SYS_KACS_IMPERSONATE_PEER` | `kacs_impersonate_peer(int sock_fd)` |
| 1012 | `SYS_KACS_REVERT` | `kacs_revert(void)` |
| 1013 | `SYS_KACS_SET_IMPERSONATION_LEVEL` | `kacs_set_impersonation_level(int sock_fd, u32 level)` |
| 1020 | `SYS_KACS_OPEN` | `kacs_open(int dirfd, const char __user *path, struct kacs_open_how __user *uhow, size_t howsize, u32 __user *status_out)` |
| 1021 | `SYS_KACS_GET_SD` | `kacs_get_sd(int dirfd, const char __user *path, u32 security_info, void __user *buf, u32 buf_len, u32 flags)` |
| 1022 | `SYS_KACS_SET_SD` | `kacs_set_sd(int dirfd, const char __user *path, u32 security_info, const void __user *sd_buf, u32 sd_len, u32 flags)` |
| 1023 | `SYS_KACS_ACCESS_CHECK` | `kacs_access_check(const void __user *uargs)` |
| 1024 | `SYS_KACS_ACCESS_CHECK_LIST` | `kacs_access_check_list(const void __user *uargs, struct kacs_node_result __user *results, u32 results_count)` |
| 1025 | `SYS_KACS_SET_CAAP` | `kacs_set_caap(const void __user *policy_sid, u32 policy_sid_len, const void __user *spec, u32 spec_len)` |
| 1026 | `SYS_KACS_GET_MOUNT_POLICY` | `kacs_get_mount_policy(int fd, struct kacs_mount_policy_args __user *uargs, size_t argsize)` |
| 1027 | `SYS_KACS_SET_MOUNT_POLICY` | `kacs_set_mount_policy(int fd, struct kacs_mount_policy_args __user *uargs, size_t argsize)` |

`uapi/pkm/syscall.h` also registers the KMES and LCS numbers,
1090–1102, documented in their own chapters.

## 3.A.2 Structure layouts

### 3.A.2.1 `struct kacs_query_args`

Total size 16 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `token_class` |
| 4 | 4 | `__u32` | `buf_len` |
| 8 | 8 | `__u64` | `buf_ptr` |

### 3.A.2.2 `struct kacs_adjust_privs_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `count` |
| 4 | 4 | `__u32` | `_pad` |
| 8 | 8 | `__u64` | `data_ptr` |
| 16 | 8 | `__u64` | `previous_enabled` |

### 3.A.2.3 `struct kacs_priv_entry`

Total size 8 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `luid` |
| 4 | 4 | `__u32` | `attributes` |

### 3.A.2.4 `struct kacs_adjust_groups_args`

Total size 144 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `count` |
| 4 | 4 | `__u32` | `_pad` |
| 8 | 8 | `__u64` | `data_ptr` |
| 16 | 128 | `__u64``[16]` | `previous_state` |

### 3.A.2.5 `struct kacs_duplicate_args`

Total size 16 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `access_mask` |
| 4 | 4 | `__u32` | `token_type` |
| 8 | 4 | `__u32` | `impersonation_level` |
| 12 | 4 | `__s32` | `result_fd` |

### 3.A.2.6 `struct kacs_group_entry`

Total size 8 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `index` |
| 4 | 4 | `__u32` | `enable` |

### 3.A.2.7 `struct kacs_adjust_default_args`

Total size 16 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 8 | `__u64` | `dacl_ptr` |
| 8 | 4 | `__u32` | `dacl_len` |
| 12 | 2 | `__u16` | `owner_index` |
| 14 | 2 | `__u16` | `group_index` |

### 3.A.2.8 `struct kacs_restrict_args`

Total size 40 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 8 | `__u64` | `privs_to_delete` |
| 8 | 4 | `__u32` | `num_deny_indices` |
| 12 | 4 | `__u32` | `num_restrict_sids` |
| 16 | 4 | `__u32` | `data_len` |
| 20 | 4 | `__u32` | `flags` |
| 24 | 8 | `__u64` | `data_ptr` |
| 32 | 4 | `__s32` | `result_fd` |
| 36 | 4 | `__u32` | `_pad` |

### 3.A.2.9 `struct kacs_link_tokens_args`

Total size 16 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__s32` | `elevated_fd` |
| 4 | 4 | `__s32` | `filtered_fd` |
| 8 | 8 | `__u64` | `logon_session_id` |

### 3.A.2.10 `struct kacs_get_linked_token_args`

Total size 4 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__s32` | `result_fd` |

### 3.A.2.11 `struct kacs_access_check_args`

Total size 136 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `caller_size` |
| 4 | 4 | `__s32` | `token_fd` |
| 8 | 8 | `__u64` | `sd_ptr` |
| 16 | 4 | `__u32` | `sd_len` |
| 20 | 4 | `__u32` | `desired_access` |
| 24 | 4 | `__u32` | `mapping_read` |
| 28 | 4 | `__u32` | `mapping_write` |
| 32 | 4 | `__u32` | `mapping_execute` |
| 36 | 4 | `__u32` | `mapping_all` |
| 40 | 8 | `__u64` | `self_sid_ptr` |
| 48 | 4 | `__u32` | `self_sid_len` |
| 52 | 4 | `__u32` | `privilege_intent` |
| 56 | 8 | `__u64` | `object_tree_ptr` |
| 64 | 4 | `__u32` | `object_tree_count` |
| 68 | 4 | `__u32` | `_pad0` |
| 72 | 8 | `__u64` | `local_claims_ptr` |
| 80 | 4 | `__u32` | `local_claims_len` |
| 84 | 4 | `__u32` | `_pad1` |
| 88 | 8 | `__u64` | `granted_out_ptr` |
| 96 | 4 | `__u32` | `pip_type` |
| 100 | 4 | `__u32` | `pip_trust` |
| 104 | 8 | `__u64` | `audit_context_ptr` |
| 112 | 4 | `__u32` | `audit_context_len` |
| 116 | 4 | `__u32` | `_pad2` |
| 120 | 8 | `__u64` | `continuous_audit_out_ptr` |
| 128 | 8 | `__u64` | `staging_mismatch_out_ptr` |

### 3.A.2.12 `struct kacs_object_type_entry`

Total size 20 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 2 | `__u16` | `level` |
| 2 | 2 | `__u16` | `_reserved` |
| 4 | 16 | `__u8``[16]` | `guid` |

### 3.A.2.13 `struct kacs_node_result`

Total size 8 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `granted` |
| 4 | 4 | `__s32` | `status` |

### 3.A.2.14 `struct kacs_open_how`

Total size 32 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `desired_access` |
| 4 | 4 | `__u32` | `create_disposition` |
| 8 | 4 | `__u32` | `create_options` |
| 12 | 4 | `__u32` | `flags` |
| 16 | 8 | `__u64` | `sd_ptr` |
| 24 | 4 | `__u32` | `sd_len` |
| 28 | 4 | `__u32` | `__pad` |

### 3.A.2.15 `struct kacs_mount_policy_args`

Total size 32 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `policy` |
| 4 | 4 | `__u32` | `flags` |
| 8 | 4 | `__u32` | `generation` |
| 12 | 4 | `__u32` | `__pad0` |
| 16 | 8 | `__u64` | `template_sd_ptr` |
| 24 | 4 | `__u32` | `template_sd_len` |
| 28 | 4 | `__u32` | `__pad1` |

### 3.A.2.16 `struct kacs_generic_mapping`

Total size 16 bytes.

| Offset | Size | Type | Field |
|---:|---:|---|---|
| 0 | 4 | `__u32` | `read` |
| 4 | 4 | `__u32` | `write` |
| 8 | 4 | `__u32` | `execute` |
| 12 | 4 | `__u32` | `all` |

## 3.A.3 Token constants

From `uapi/pkm/token.h`.

*kacs_open_self_token (SYS_KACS_OPEN_SELF_TOKEN) flags.*

| Constant | Value |
|---|---|
| `KACS_TOKEN_OPEN_REAL` | `0x01` (1) |

*Per-handle token rights (the low 16 bits of a token access mask).*

| Constant | Value |
|---|---|
| `KACS_TOKEN_ASSIGN_PRIMARY` | `0x0001` (1) |
| `KACS_TOKEN_DUPLICATE` | `0x0002` (2) |
| `KACS_TOKEN_IMPERSONATE` | `0x0004` (4) |
| `KACS_TOKEN_QUERY` | `0x0008` (8) |
| `KACS_TOKEN_QUERY_SOURCE` | `0x0010` (16) |
| `KACS_TOKEN_ADJUST_PRIVS` | `0x0020` (32) |
| `KACS_TOKEN_ADJUST_GROUPS` | `0x0040` (64) |
| `KACS_TOKEN_ADJUST_DEFAULT` | `0x0080` (128) |
| `KACS_TOKEN_ADJUST_INTERACTIVITY_SCOPE` | `0x0100` (256) |
| `KACS_TOKEN_ALL_ACCESS` | `0x000F01FF` |

*Token ioctl interface identifier.*

| Constant | Value |
|---|---|
| `KACS_IOC_MAGIC` | `0x4B` (75) |

*kacs_priv_entry.attributes bits.*

| Constant | Value |
|---|---|
| `KACS_PRIVILEGE_ATTR_ENABLED` | `0x00000002` (2) |
| `KACS_PRIVILEGE_ATTR_REMOVED` | `0x00000004` (4) |

*kacs_adjust_privs bulk-reset flag (not a per-entry attribute).*

| Constant | Value |
|---|---|
| `KACS_PRIVILEGE_RESET_ALL_DEFAULTS` | `0x80000000` |

*kacs_restrict_args.flags bits.*

| Constant | Value |
|---|---|
| `KACS_TOKEN_RESTRICT_WRITE_RESTRICTED` | `0x00000001` (1) |

*Token type (KACS_TOKEN_CLASS_TYPE).*

| Constant | Value |
|---|---|
| `KACS_TOKEN_TYPE_PRIMARY` | `0x01` (1) |
| `KACS_TOKEN_TYPE_IMPERSONATION` | `0x02` (2) |

*Impersonation level (KACS_TOKEN_CLASS_IMPERSONATION_LEVEL).*

| Constant | Value |
|---|---|
| `KACS_IMLEVEL_ANONYMOUS` | `0x00` (0) |
| `KACS_IMLEVEL_IDENTIFICATION` | `0x01` (1) |
| `KACS_IMLEVEL_IMPERSONATION` | `0x02` (2) |
| `KACS_IMLEVEL_DELEGATION` | `0x03` (3) |

*Elevation type (KACS_TOKEN_CLASS_ELEVATION_TYPE).*

| Constant | Value |
|---|---|
| `KACS_ELEVATION_DEFAULT` | `0x01` (1) |
| `KACS_ELEVATION_FULL` | `0x02` (2) |
| `KACS_ELEVATION_LIMITED` | `0x03` (3) |

*Mandatory-policy bits (KACS_TOKEN_CLASS_MANDATORY_POLICY).*

| Constant | Value |
|---|---|
| `KACS_TOKEN_MANDATORY_POLICY_NO_WRITE_UP` | `0x00000001` (1) |
| `KACS_TOKEN_MANDATORY_POLICY_NEW_PROCESS_MIN` | `0x00000002` (2) |

Per-token audit-policy bits — the create-token spec `audit_policy` field
(KACS_TOKEN_SPEC_OFF_AUDIT_POLICY). They select which access-check
outcomes the token's object accesses generate audit events for.

| Constant | Value |
|---|---|
| `KACS_AUDIT_POLICY_OBJECT_ACCESS_SUCCESS` | `0x00000001` (1) |
| `KACS_AUDIT_POLICY_OBJECT_ACCESS_FAILURE` | `0x00000002` (2) |
| `KACS_AUDIT_POLICY_PRIVILEGE_USE_SUCCESS` | `0x00000004` (4) |
| `KACS_AUDIT_POLICY_PRIVILEGE_USE_FAILURE` | `0x00000008` (8) |

*Logon type (KACS_TOKEN_CLASS_LOGON_TYPE).*

| Constant | Value |
|---|---|
| `KACS_LOGON_TYPE_INTERACTIVE` | `2` |
| `KACS_LOGON_TYPE_NETWORK` | `3` |
| `KACS_LOGON_TYPE_BATCH` | `4` |
| `KACS_LOGON_TYPE_SERVICE` | `5` |
| `KACS_LOGON_TYPE_NETWORK_CLEARTEXT` | `8` |
| `KACS_LOGON_TYPE_NEW_CREDENTIALS` | `9` |

*Maximum number of groups a token may carry.*

| Constant | Value |
|---|---|
| `KACS_TOKEN_MAX_GROUPS` | `1024` |

*Number of 64-bit words in a group enabled-state bitmask (KACS_TOKEN_MAX_GROUPS / 64).*

| Constant | Value |
|---|---|
| `KACS_TOKEN_GROUP_MASK_WORDS` | `16` |

*kacs_create_token (SYS_KACS_CREATE_TOKEN) spec wire format.*

The (spec, len) buffer the syscall consumes is a fixed
KACS_TOKEN_SPEC_HEADER_BYTES-byte header followed by variable-length
sections at header-specified byte offsets. An offset/length (or
offset/count) pair that is both zero means the section is absent.
Sections may appear in any order; every offset+length is validated to
fall within the buffer. The header is not a C struct (it carries packed
mixed-width fields and crosses the syscall boundary as raw bytes); read
each field at its KACS_TOKEN_SPEC_OFF_* offset. Its fields, in order:
__u32 version must be KACS_TOKEN_SPEC_VERSION __u8 token_type
KACS_TOKEN_TYPE_ __u8 impersonation_level KACS_IMLEVEL_ __u8
_reserved0[2] must be 0 __u32 integrity_rid integrity-level RID __u32
mandatory_policy KACS_TOKEN_MANDATORY_POLICY_* bits __u64 privs_present
privilege bitmask (KACS_SE_*_PRIVILEGE) __u64 privs_enabled initially
enabled privileges (subset) __u32 _reserved1 must be 0 (elevation set
only by LINK_TOKENS) __u32 projected_uid Linux UID for credential
projection __u32 projected_gid Linux GID for credential projection __u32
audit_policy per-token audit flags __u64 expiration expiry timestamp (0
= none) __u64 logon_session_id logon session ID (auth_id) __u32
owner_sid_index 0 = user SID, 1..N = caller group __u32
primary_group_index 0 = user SID, 1..N = caller group __u8
source_name[8] token source name __u64 source_id token source LUID __u32
user_sid_offset byte offset to the user SID __u32 groups_offset byte
offset to the groups array __u32 groups_count number of group entries
__u32 default_dacl_offset byte offset to the default DACL (0 = none)
__u32 default_dacl_len default DACL byte length (0 = none) __u32
user_claims_offset byte offset to user claims (0 = none) __u32
user_claims_len user claims byte length (0 = none) __u32
device_claims_offset byte offset to device claims (0 = none) __u32
device_claims_len device claims byte length (0 = none) __u32
device_groups_offset byte offset to device groups (0 = none) __u32
device_groups_count number of device-group entries (0 = none) __u32
restricted_sids_offset byte offset to restricted SIDs (0 = none) __u32
restricted_sids_count number of restricted-SID entries (0 = none) __u32
confinement_sid_offset byte offset to confinement SID (0 = none) __u32
confinement_sid_len confinement SID byte length (0 = none) __u32
confinement_caps_offset byte offset to confinement caps (0 = none) __u32
confinement_caps_count number of confinement-cap entries (0 = none) __u8
confinement_exempt 1 = exempt from confinement __u8 write_restricted 1 =
write-restricted mode __u8 user_deny_only 1 = user SID matches deny ACEs
only __u8 isolation_boundary 1 = enable namespace filtering __u32
supp_gids_offset byte offset to supplementary GIDs (0 = none) __u32
supp_gids_count number of supplementary-GID entries (0 = none) __u32
restricted_device_groups_offset byte offset (0 = none) __u32
restricted_device_groups_count entry count (0 = none) __u64 origin
originating logon-session LUID (0 = none) __u32 interactivity_scope
interactive session number __u32 lcs_credentials_offset byte offset to
the LCS extension (0 = none) A group/device-group/restricted-
SID/confinement-cap/restricted-device-group entry is [__u32
sid_len][__u8 sid[sid_len]][__u32 attributes]. A supplementary-GIDs
section is supp_gids_count little-endian __u32 GIDs. All multi-byte
header and section scalars are little-endian.

| Constant | Value |
|---|---|
| `KACS_TOKEN_SPEC_VERSION` | `2` |
| `KACS_TOKEN_SPEC_HEADER_BYTES` | `192` |
| `KACS_TOKEN_SPEC_MIN_BYTES` | `192` |
| `KACS_TOKEN_SPEC_MAX_BYTES` | `65536` |

*Byte offsets of the fixed token-spec header fields.*

| Constant | Value |
|---|---|
| `KACS_TOKEN_SPEC_OFF_VERSION` | `0` |
| `KACS_TOKEN_SPEC_OFF_TOKEN_TYPE` | `4` |
| `KACS_TOKEN_SPEC_OFF_IMPERSONATION_LEVEL` | `5` |
| `KACS_TOKEN_SPEC_OFF_RESERVED0` | `6` |
| `KACS_TOKEN_SPEC_OFF_INTEGRITY_RID` | `8` |
| `KACS_TOKEN_SPEC_OFF_MANDATORY_POLICY` | `12` |
| `KACS_TOKEN_SPEC_OFF_PRIVS_PRESENT` | `16` |
| `KACS_TOKEN_SPEC_OFF_PRIVS_ENABLED` | `24` |
| `KACS_TOKEN_SPEC_OFF_RESERVED1` | `32` |
| `KACS_TOKEN_SPEC_OFF_PROJECTED_UID` | `36` |
| `KACS_TOKEN_SPEC_OFF_PROJECTED_GID` | `40` |
| `KACS_TOKEN_SPEC_OFF_AUDIT_POLICY` | `44` |
| `KACS_TOKEN_SPEC_OFF_EXPIRATION` | `48` |
| `KACS_TOKEN_SPEC_OFF_LOGON_SESSION_ID` | `56` |
| `KACS_TOKEN_SPEC_OFF_OWNER_SID_INDEX` | `64` |
| `KACS_TOKEN_SPEC_OFF_PRIMARY_GROUP_INDEX` | `68` |
| `KACS_TOKEN_SPEC_OFF_SOURCE_NAME` | `72` |
| `KACS_TOKEN_SPEC_OFF_SOURCE_ID` | `80` |
| `KACS_TOKEN_SPEC_OFF_USER_SID_OFFSET` | `88` |
| `KACS_TOKEN_SPEC_OFF_GROUPS_OFFSET` | `92` |
| `KACS_TOKEN_SPEC_OFF_GROUPS_COUNT` | `96` |
| `KACS_TOKEN_SPEC_OFF_DEFAULT_DACL_OFFSET` | `100` |
| `KACS_TOKEN_SPEC_OFF_DEFAULT_DACL_LEN` | `104` |
| `KACS_TOKEN_SPEC_OFF_USER_CLAIMS_OFFSET` | `108` |
| `KACS_TOKEN_SPEC_OFF_USER_CLAIMS_LEN` | `112` |
| `KACS_TOKEN_SPEC_OFF_DEVICE_CLAIMS_OFFSET` | `116` |
| `KACS_TOKEN_SPEC_OFF_DEVICE_CLAIMS_LEN` | `120` |
| `KACS_TOKEN_SPEC_OFF_DEVICE_GROUPS_OFFSET` | `124` |
| `KACS_TOKEN_SPEC_OFF_DEVICE_GROUPS_COUNT` | `128` |
| `KACS_TOKEN_SPEC_OFF_RESTRICTED_SIDS_OFFSET` | `132` |
| `KACS_TOKEN_SPEC_OFF_RESTRICTED_SIDS_COUNT` | `136` |
| `KACS_TOKEN_SPEC_OFF_CONFINEMENT_SID_OFFSET` | `140` |
| `KACS_TOKEN_SPEC_OFF_CONFINEMENT_SID_LEN` | `144` |
| `KACS_TOKEN_SPEC_OFF_CONFINEMENT_CAPS_OFFSET` | `148` |
| `KACS_TOKEN_SPEC_OFF_CONFINEMENT_CAPS_COUNT` | `152` |
| `KACS_TOKEN_SPEC_OFF_CONFINEMENT_EXEMPT` | `156` |
| `KACS_TOKEN_SPEC_OFF_WRITE_RESTRICTED` | `157` |
| `KACS_TOKEN_SPEC_OFF_USER_DENY_ONLY` | `158` |
| `KACS_TOKEN_SPEC_OFF_ISOLATION_BOUNDARY` | `159` |
| `KACS_TOKEN_SPEC_OFF_SUPP_GIDS_OFFSET` | `160` |
| `KACS_TOKEN_SPEC_OFF_SUPP_GIDS_COUNT` | `164` |
| `KACS_TOKEN_SPEC_OFF_RESTRICTED_DEVICE_GROUPS_OFFSET` | `168` |
| `KACS_TOKEN_SPEC_OFF_RESTRICTED_DEVICE_GROUPS_COUNT` | `172` |
| `KACS_TOKEN_SPEC_OFF_ORIGIN` | `176` |
| `KACS_TOKEN_SPEC_OFF_INTERACTIVITY_SCOPE` | `184` |
| `KACS_TOKEN_SPEC_OFF_LCS_CREDENTIALS_OFFSET` | `188` |

*Byte length of the fixed token-source-name field.*

| Constant | Value |
|---|---|
| `KACS_TOKEN_SPEC_SOURCE_NAME_BYTES` | `8` |

Optional LCS registry-credentials extension, located at the token-spec
header's lcs_credentials_offset. The section is a fixed
KACS_TOKEN_LCS_EXT_HEADER_BYTES-byte header bounded by the next active
variable-section offset or the end of the spec; it is consumed exactly
(trailing bytes are malformed). Header fields, in order: __u32 version
must be KACS_TOKEN_LCS_EXT_VERSION __u32 _reserved must be 0 __u32
scope_count private hive scope GUIDs (<= max) __u32 private_layer_count
private layer names (<= max) Payload: scope_count raw 16-byte GUIDs,
then private_layer_count little-endian __u32 name byte lengths, then the
concatenated UTF-8 layer names. Scope GUIDs must be non-nil and unique;
layer names must be 1.. KACS_TOKEN_LCS_MAX_LAYER_NAME_BYTES bytes, must
not contain '\\', '/', or NUL, and must be unique under case-insensitive
matching.

| Constant | Value |
|---|---|
| `KACS_TOKEN_LCS_EXT_VERSION` | `1` |
| `KACS_TOKEN_LCS_EXT_HEADER_BYTES` | `16` |
| `KACS_TOKEN_LCS_SCOPE_GUID_BYTES` | `16` |
| `KACS_TOKEN_LCS_MAX_SCOPE_GUIDS` | `256` |
| `KACS_TOKEN_LCS_MAX_PRIVATE_LAYERS` | `256` |
| `KACS_TOKEN_LCS_MAX_LAYER_NAME_BYTES` | `255` |

*Byte offsets of the fixed LCS-extension header fields.*

| Constant | Value |
|---|---|
| `KACS_TOKEN_LCS_EXT_OFF_VERSION` | `0` |
| `KACS_TOKEN_LCS_EXT_OFF_RESERVED` | `4` |
| `KACS_TOKEN_LCS_EXT_OFF_SCOPE_COUNT` | `8` |
| `KACS_TOKEN_LCS_EXT_OFF_PRIVATE_LAYER_COUNT` | `12` |

*kacs_create_logon_session (SYS_KACS_CREATE_LOGON_SESSION) spec wire format.*

The (spec, len) buffer the syscall consumes is, in order: __u8
logon_type one of KACS_LOGON_TYPE_* above __le16 auth_pkg_len byte
length of the auth-package name __u8 auth_pkg[auth_pkg_len] auth-package
name (valid UTF-8) __le32 user_sid_len byte length of the user SID __u8
user_sid[user_sid_len] binary SID of the authenticated user The buffer
is consumed exactly: 7 + auth_pkg_len + user_sid_len must equal len. The
kernel assigns the session ID and derives the logon SID from it.

| Constant | Value |
|---|---|
| `KACS_LOGON_SESSION_SPEC_MIN_BYTES` | `15` |
| `KACS_LOGON_SESSION_SPEC_MAX_BYTES` | `4096` |

*Byte offsets of the fixed-position session-spec fields.*

| Constant | Value |
|---|---|
| `KACS_LOGON_SESSION_SPEC_OFF_LOGON_TYPE` | `0` |
| `KACS_LOGON_SESSION_SPEC_OFF_AUTH_PKG_LEN` | `1` |
| `KACS_LOGON_SESSION_SPEC_OFF_AUTH_PKG` | `3` |

*Token-handle ioctls.*

| Constant | Value |
|---|---|
| `KACS_IOC_QUERY` | `0xC0104B00` |
| `KACS_IOC_ADJUST_PRIVS` | `0x40184B01` |
| `KACS_IOC_DUPLICATE` | `0xC0104B02` |
| `KACS_IOC_INSTALL` | `0x00004B03` |
| `KACS_IOC_RESTRICT` | `0xC0284B04` |
| `KACS_IOC_LINK_TOKENS` | `0x40104B05` |
| `KACS_IOC_GET_LINKED_TOKEN` | `0xC0044B06` |
| `KACS_IOC_ADJUST_GROUPS` | `0x40904B07` |
| `KACS_IOC_IMPERSONATE` | `0x00004B08` |
| `KACS_IOC_ADJUST_DEFAULT` | `0x40104B09` |
| `KACS_IOC_ADJUST_INTERACTIVITY_SCOPE` | `0x40044B0A` |

*Token information classes (kacs_query_args.token_class).*

| Constant | Value |
|---|---|
| `KACS_TOKEN_CLASS_USER` | `0x01` (1) |
| `KACS_TOKEN_CLASS_GROUPS` | `0x02` (2) |
| `KACS_TOKEN_CLASS_PRIVILEGES` | `0x03` (3) |
| `KACS_TOKEN_CLASS_TYPE` | `0x04` (4) |
| `KACS_TOKEN_CLASS_INTEGRITY_LEVEL` | `0x05` (5) |
| `KACS_TOKEN_CLASS_OWNER` | `0x06` (6) |
| `KACS_TOKEN_CLASS_PRIMARY_GROUP` | `0x07` (7) |
| `KACS_TOKEN_CLASS_INTERACTIVITY_SCOPE` | `0x08` (8) |
| `KACS_TOKEN_CLASS_RESTRICTED_SIDS` | `0x09` (9) |
| `KACS_TOKEN_CLASS_SOURCE` | `0x0A` (10) |
| `KACS_TOKEN_CLASS_STATISTICS` | `0x0B` (11) |
| `KACS_TOKEN_CLASS_ORIGIN` | `0x0C` (12) |
| `KACS_TOKEN_CLASS_ELEVATION_TYPE` | `0x0D` (13) |
| `KACS_TOKEN_CLASS_DEVICE_GROUPS` | `0x0E` (14) |
| `KACS_TOKEN_CLASS_APPCONTAINER_SID` | `0x0F` (15) |
| `KACS_TOKEN_CLASS_CAPABILITIES` | `0x10` (16) |
| `KACS_TOKEN_CLASS_MANDATORY_POLICY` | `0x11` (17) |
| `KACS_TOKEN_CLASS_LOGON_TYPE` | `0x12` (18) |
| `KACS_TOKEN_CLASS_LOGON_SID` | `0x13` (19) |
| `KACS_TOKEN_CLASS_DEFAULT_DACL` | `0x14` (20) |
| `KACS_TOKEN_CLASS_IMPERSONATION_LEVEL` | `0x15` (21) |
| `KACS_TOKEN_CLASS_USER_CLAIMS` | `0x16` (22) |
| `KACS_TOKEN_CLASS_DEVICE_CLAIMS` | `0x17` (23) |
| `KACS_TOKEN_CLASS_PROJECTED_SUPPLEMENTARY_GIDS` | `0x18` (24) |

Privileges, as single-bit masks within a token's 64-bit privilege word
(present / enabled / enabled-by-default / used are each one such word).
Named for the Windows privilege identifiers (SeTcbPrivilege, …).

| Constant | Value |
|---|---|
| `KACS_SE_CREATE_TOKEN_PRIVILEGE` | `4` |
| `KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE` | `8` |
| `KACS_SE_LOCK_MEMORY_PRIVILEGE` | `16` |
| `KACS_SE_INCREASE_QUOTA_PRIVILEGE` | `32` |
| `KACS_SE_TCB_PRIVILEGE` | `128` |
| `KACS_SE_SECURITY_PRIVILEGE` | `256` |
| `KACS_SE_TAKE_OWNERSHIP_PRIVILEGE` | `512` |
| `KACS_SE_LOAD_DRIVER_PRIVILEGE` | `1024` |
| `KACS_SE_SYSTEM_PROFILE_PRIVILEGE` | `2048` |
| `KACS_SE_SYSTEMTIME_PRIVILEGE` | `4096` |
| `KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE` | `8192` |
| `KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE` | `16384` |
| `KACS_SE_BACKUP_PRIVILEGE` | `131072` |
| `KACS_SE_RESTORE_PRIVILEGE` | `262144` |
| `KACS_SE_SHUTDOWN_PRIVILEGE` | `524288` |
| `KACS_SE_DEBUG_PRIVILEGE` | `0x100000` (1048576) |
| `KACS_SE_AUDIT_PRIVILEGE` | `0x200000` (2097152) |
| `KACS_SE_CHANGE_NOTIFY_PRIVILEGE` | `0x800000` (8388608) |
| `KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE` | `0x1000000` (16777216) |
| `KACS_SE_MANAGE_VOLUME_PRIVILEGE` | `0x10000000` (268435456) |
| `KACS_SE_IMPERSONATE_PRIVILEGE` | `0x20000000` (536870912) |
| `KACS_SE_RELABEL_PRIVILEGE` | `0x100000000` (4294967296) |
| `KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE` | `0x800000000` (34359738368) |
| `KACS_SE_BIND_PRIVILEGED_PORT_PRIVILEGE` | `0x8000000000000000` (9223372036854775808) |

## 3.A.4 AccessCheck constants

From `uapi/pkm/access.h`.

*Full size of kacs_access_check_args the current kernel copies.*

| Constant | Value |
|---|---|
| `KACS_ACCESS_CHECK_ARGS_SIZE` | `136` |

*Minimum caller_size the kernel accepts (the v1 / v0.20 layout).*

| Constant | Value |
|---|---|
| `KACS_ACCESS_CHECK_ARGS_V1_SIZE` | `40` |

*Byte size of one kacs_object_type_entry in the object-type tree array.*

| Constant | Value |
|---|---|
| `KACS_OBJECT_TYPE_ENTRY_SIZE` | `20` |

*Largest object-audit-context buffer the kernel accepts.*

| Constant | Value |
|---|---|
| `KACS_ACCESS_CHECK_MAX_AUDIT_CONTEXT_LEN` | `4096` |

*Largest @Local claims blob the kernel accepts (local_claims_ptr).*

| Constant | Value |
|---|---|
| `KACS_ACCESS_CHECK_MAX_LOCAL_CLAIMS_LEN` | `65536` |

*Largest object-type tree entry count the kernel accepts.*

| Constant | Value |
|---|---|
| `KACS_ACCESS_CHECK_MAX_OBJECT_TYPE_COUNT` | `1024` |

Claim value types — the discriminant of one attribute in the @Local
claims array (local_claims_ptr).

| Constant | Value |
|---|---|
| `KACS_CLAIM_TYPE_INT64` | `0x0001` (1) |
| `KACS_CLAIM_TYPE_UINT64` | `0x0002` (2) |
| `KACS_CLAIM_TYPE_STRING` | `0x0003` (3) |
| `KACS_CLAIM_TYPE_SID` | `0x0005` (5) |
| `KACS_CLAIM_TYPE_BOOLEAN` | `0x0006` (6) |
| `KACS_CLAIM_TYPE_OCTET` | `0x0010` (16) |

*Claim attribute flags.*

| Constant | Value |
|---|---|
| `KACS_CLAIM_ATTR_CASE_SENSITIVE` | `0x0002` (2) |
| `KACS_CLAIM_ATTR_USE_FOR_DENY_ONLY` | `0x0004` (4) |
| `KACS_CLAIM_ATTR_DISABLED` | `0x0010` (16) |

Central Access Policy (CAAP) spec wire format — the (spec, spec_len)
buffer kacs_set_caap (SYS_KACS_SET_CAAP) consumes. A non-empty spec
replaces the policy identified by the call's policy SID; a NULL/zero
spec removes it. The buffer is a fixed prefix followed by rule_count
per-rule sections, and is consumed exactly (trailing bytes are
rejected). It is not a C struct (the rule sections are variable-length);
read it as raw bytes: __u8 version must be KACS_CAAP_SPEC_VERSION __le32
rule_count number of rules that follow (<= max) rule_count * { __le32
applies_to_len [__u8 applies_to[applies_to_len]] conditional-expression
bytecode; 0 = always __le32 effective_dacl_len [__u8
effective_dacl[...]] binary ACL; length MUST be nonzero __le32
effective_sacl_len [__u8 effective_sacl[...]] (0 = none) __le32
staged_dacl_len [__u8 staged_dacl[...]] (0 = none) __le32
staged_sacl_len [__u8 staged_sacl[...]] (0 = none) } Every length-
prefixed field uses a little-endian __u32 length and is bounded by
KACS_CAAP_MAX_FIELD_BYTES; ACL payloads additionally parse under the
security-descriptor size limit (KACS_CAAP_MAX_ACL_BYTES). ACLs use the
binary ACL format from <pkm/sd.h>; applies_to is conditional-ACE
bytecode.

| Constant | Value |
|---|---|
| `KACS_CAAP_SPEC_VERSION` | `0x01` (1) |
| `KACS_CAAP_MAX_SPEC_BYTES` | `262144` |
| `KACS_CAAP_MAX_RULE_COUNT` | `256` |
| `KACS_CAAP_MAX_FIELD_BYTES` | `65536` |
| `KACS_CAAP_MAX_ACL_BYTES` | `65535` |

*Byte offsets of the fixed CAAP-spec prefix fields.*

| Constant | Value |
|---|---|
| `KACS_CAAP_SPEC_OFF_VERSION` | `0` |
| `KACS_CAAP_SPEC_OFF_RULE_COUNT` | `1` |

*Byte length of the fixed CAAP-spec prefix (version + rule_count).*

| Constant | Value |
|---|---|
| `KACS_CAAP_SPEC_PREFIX_BYTES` | `5` |

## 3.A.5 File and open constants

From `uapi/pkm/file.h`.

*Minimum caller-supplied size accepted for each argument block.*

| Constant | Value |
|---|---|
| `KACS_OPEN_HOW_MIN_SIZE` | `16` |
| `KACS_MOUNT_POLICY_ARGS_MIN_SIZE` | `16` |

*Create dispositions (kacs_open_how.create_disposition).*

| Constant | Value |
|---|---|
| `KACS_DISPOSITION_SUPERSEDE` | `0` |
| `KACS_DISPOSITION_OPEN` | `1` |
| `KACS_DISPOSITION_CREATE` | `2` |
| `KACS_DISPOSITION_OPEN_IF` | `3` |
| `KACS_DISPOSITION_OVERWRITE` | `4` |
| `KACS_DISPOSITION_OVERWRITE_IF` | `5` |

*Create options (kacs_open_how.create_options).*

| Constant | Value |
|---|---|
| `KACS_CREATE_OPT_DIRECTORY` | `0x0001` (1) |
| `KACS_CREATE_OPT_DELETE_ON_CLOSE` | `0x0002` (2) |

*kacs_open_how.flags bits.*

| Constant | Value |
|---|---|
| `KACS_BACKUP_INTENT` | `0x00000001` (1) |
| `KACS_RESTORE_INTENT` | `0x00000002` (2) |

*File and directory object-specific access rights (the low 16 bits of a file access mask).*

The directory aliases name the same bit as the file right it acts as for
a directory object.

| Constant | Value |
|---|---|
| `KACS_FILE_READ_DATA` | `0x00000001` (1) |
| `KACS_FILE_WRITE_DATA` | `0x00000002` (2) |
| `KACS_FILE_APPEND_DATA` | `0x00000004` (4) |
| `KACS_FILE_READ_EA` | `0x00000008` (8) |
| `KACS_FILE_WRITE_EA` | `0x00000010` (16) |
| `KACS_FILE_EXECUTE` | `0x00000020` (32) |
| `KACS_FILE_DELETE_CHILD` | `0x00000040` (64) |
| `KACS_FILE_READ_ATTRIBUTES` | `0x00000080` (128) |
| `KACS_FILE_WRITE_ATTRIBUTES` | `0x00000100` (256) |
| `KACS_FILE_LIST_DIRECTORY` | `1` |
| `KACS_FILE_TRAVERSE` | `32` |
| `KACS_FILE_ADD_FILE` | `2` |
| `KACS_FILE_ADD_SUBDIRECTORY` | `4` |

*Mount-policy values (kacs_mount_policy_args.policy).*

| Constant | Value |
|---|---|
| `KACS_MOUNT_POLICY_UNMANAGED` | `1` |
| `KACS_MOUNT_POLICY_DENY_MISSING` | `2` |
| `KACS_MOUNT_POLICY_SYNTHESIZE_EPHEMERAL` | `3` |
| `KACS_MOUNT_POLICY_SYNTHESIZE_PERSISTENT` | `4` |

*Status word kacs_open writes back, describing what happened to the file.*

| Constant | Value |
|---|---|
| `KACS_STATUS_OPENED` | `1` |
| `KACS_STATUS_CREATED` | `2` |
| `KACS_STATUS_OVERWRITTEN` | `3` |
| `KACS_STATUS_SUPERSEDED` | `4` |

## 3.A.6 Process access rights

From `uapi/pkm/process.h`.

*KACS process object-specific access rights (the low 16 bits of a process access mask).*

Named for the Windows process rights; an access check folds the generic
bits (<pkm/sd.h>) into these via the process generic mapping.

| Constant | Value |
|---|---|
| `KACS_PROCESS_TERMINATE` | `0x00000001` (1) |
| `KACS_PROCESS_SIGNAL` | `0x00000002` (2) |
| `KACS_PROCESS_VM_READ` | `0x00000010` (16) |
| `KACS_PROCESS_VM_WRITE` | `0x00000020` (32) |
| `KACS_PROCESS_DUP_HANDLE` | `0x00000040` (64) |
| `KACS_PROCESS_SET_INFORMATION` | `0x00000200` (512) |
| `KACS_PROCESS_QUERY_INFORMATION` | `0x00000400` (1024) |
| `KACS_PROCESS_SUSPEND_RESUME` | `0x00000800` (2048) |
| `KACS_PROCESS_QUERY_LIMITED` | `0x00001000` (4096) |

## 3.A.7 Process mitigation bits

From `uapi/pkm/psb.h`.

*Process Security Block (PSB) process-mitigation bits.*

The `mitigations` argument of kacs_set_psb (SYS_KACS_SET_PSB) is a
bitmask of these flags. Setting a bit is activation-backed: KACS either
places the target process in the protected state (or verifies it already
satisfies the invariant) before committing, and rejects later operations
that would disable the protection. Each mitigation is enforced at its
own enforcement point and persists across exec. Only the bits in
KACS_MIT_ALL are valid; any other bit set in the request is rejected.
KACS_MIT_CFI is a legacy alias: requesting it sets both KACS_MIT_CFIF
and KACS_MIT_CFIB, and the alias bit itself is not retained.

| Constant | Value | Notes |
|---|---|---|
| `KACS_MIT_WXP` | `0x001` (1) | Write-XOR-Execute protection |
| `KACS_MIT_TLP` | `0x002` (2) | Trusted Library Paths |
| `KACS_MIT_LSV` | `0x004` (4) | Library Signature Verification |
| `KACS_MIT_CFI` | `0x008` (8) | legacy alias: CFIF \| CFIB |
| `KACS_MIT_UI_ACCESS` | `0x010` (16) | UI interaction (reserved) |
| `KACS_MIT_NO_CHILD` | `0x020` (32) | cannot fork (one-way) |
| `KACS_MIT_CFIF` | `0x040` (64) | forward-edge CFI (Intel IBT) |
| `KACS_MIT_CFIB` | `0x080` (128) | backward-edge CFI (shadow stack) |
| `KACS_MIT_PIE` | `0x100` (256) | reject non-PIE binaries at exec |
| `KACS_MIT_SML` | `0x200` (512) | speculation mitigation lock |

*All valid mitigation bits OR'd together — the accepted-request mask.*

| Constant | Value |
|---|---|
| `KACS_MIT_ALL` | `0x3FF` (1023) |

## 3.A.8 Security descriptor constants

From `uapi/pkm/sd.h`.

*Byte length of the self-relative security-descriptor header.*

| Constant | Value |
|---|---|
| `KACS_SD_HEADER_BYTES` | `20` |

SECURITY_INFORMATION selector bits — which components of a security
descriptor a kacs_get_sd / kacs_set_sd call reads or writes.

| Constant | Value |
|---|---|
| `KACS_SECINFO_OWNER` | `0x00000001` (1) |
| `KACS_SECINFO_GROUP` | `0x00000002` (2) |
| `KACS_SECINFO_DACL` | `0x00000004` (4) |
| `KACS_SECINFO_SACL` | `0x00000008` (8) |
| `KACS_SECINFO_LABEL` | `0x00000010` (16) |

*SECURITY_DESCRIPTOR_CONTROL bits — the SD header `control` field.*

| Constant | Value |
|---|---|
| `KACS_SD_OWNER_DEFAULTED` | `0x0001` (1) |
| `KACS_SD_GROUP_DEFAULTED` | `0x0002` (2) |
| `KACS_SD_DACL_PRESENT` | `0x0004` (4) |
| `KACS_SD_DACL_DEFAULTED` | `0x0008` (8) |
| `KACS_SD_SACL_PRESENT` | `0x0010` (16) |
| `KACS_SD_SACL_DEFAULTED` | `0x0020` (32) |
| `KACS_SD_DACL_TRUSTED` | `0x0040` (64) |
| `KACS_SD_SERVER_SECURITY` | `0x0080` (128) |
| `KACS_SD_DACL_AUTO_INHERIT_REQ` | `0x0100` (256) |
| `KACS_SD_SACL_AUTO_INHERIT_REQ` | `0x0200` (512) |
| `KACS_SD_DACL_AUTO_INHERITED` | `0x0400` (1024) |
| `KACS_SD_SACL_AUTO_INHERITED` | `0x0800` (2048) |
| `KACS_SD_DACL_PROTECTED` | `0x1000` (4096) |
| `KACS_SD_SACL_PROTECTED` | `0x2000` (8192) |
| `KACS_SD_RM_CONTROL_VALID` | `0x4000` (16384) |
| `KACS_SD_SELF_RELATIVE` | `0x8000` (32768) |

*Access-mask bits — standard rights (bits 16-24) and generic rights (bits 28-31).*

The low 16 bits of a mask are object-class specific; see <pkm/file.h>
and <pkm/token.h> for those.

| Constant | Value |
|---|---|
| `KACS_ACCESS_DELETE` | `0x00010000` (65536) |
| `KACS_ACCESS_READ_CONTROL` | `0x00020000` |
| `KACS_ACCESS_WRITE_DAC` | `0x00040000` |
| `KACS_ACCESS_WRITE_OWNER` | `0x00080000` |
| `KACS_ACCESS_SYNCHRONIZE` | `0x00100000` |
| `KACS_ACCESS_ACCESS_SYSTEM_SECURITY` | `0x01000000` |
| `KACS_ACCESS_MAXIMUM_ALLOWED` | `0x02000000` |
| `KACS_ACCESS_GENERIC_ALL` | `0x10000000` |
| `KACS_ACCESS_GENERIC_EXECUTE` | `0x20000000` |
| `KACS_ACCESS_GENERIC_WRITE` | `0x40000000` |
| `KACS_ACCESS_GENERIC_READ` | `0x80000000` |

*ACE types — the `ace_type` byte of an ACE header (MS-DTYP 2.4.4.1).*

| Constant | Value |
|---|---|
| `KACS_ACE_TYPE_ACCESS_ALLOWED` | `0x00` (0) |
| `KACS_ACE_TYPE_ACCESS_DENIED` | `0x01` (1) |
| `KACS_ACE_TYPE_SYSTEM_AUDIT` | `0x02` (2) |
| `KACS_ACE_TYPE_SYSTEM_ALARM` | `0x03` (3) |
| `KACS_ACE_TYPE_ACCESS_ALLOWED_COMPOUND` | `0x04` (4) |
| `KACS_ACE_TYPE_ACCESS_ALLOWED_OBJECT` | `0x05` (5) |
| `KACS_ACE_TYPE_ACCESS_DENIED_OBJECT` | `0x06` (6) |
| `KACS_ACE_TYPE_SYSTEM_AUDIT_OBJECT` | `0x07` (7) |
| `KACS_ACE_TYPE_SYSTEM_ALARM_OBJECT` | `0x08` (8) |
| `KACS_ACE_TYPE_ACCESS_ALLOWED_CALLBACK` | `0x09` (9) |
| `KACS_ACE_TYPE_ACCESS_DENIED_CALLBACK` | `0x0A` (10) |
| `KACS_ACE_TYPE_ACCESS_ALLOWED_CALLBACK_OBJECT` | `0x0B` (11) |
| `KACS_ACE_TYPE_ACCESS_DENIED_CALLBACK_OBJECT` | `0x0C` (12) |
| `KACS_ACE_TYPE_SYSTEM_AUDIT_CALLBACK` | `0x0D` (13) |
| `KACS_ACE_TYPE_SYSTEM_ALARM_CALLBACK` | `0x0E` (14) |
| `KACS_ACE_TYPE_SYSTEM_AUDIT_CALLBACK_OBJECT` | `0x0F` (15) |
| `KACS_ACE_TYPE_SYSTEM_ALARM_CALLBACK_OBJECT` | `0x10` (16) |
| `KACS_ACE_TYPE_SYSTEM_MANDATORY_LABEL` | `0x11` (17) |
| `KACS_ACE_TYPE_SYSTEM_RESOURCE_ATTRIBUTE` | `0x12` (18) |
| `KACS_ACE_TYPE_SYSTEM_SCOPED_POLICY_ID` | `0x13` (19) |
| `KACS_ACE_TYPE_SYSTEM_PROCESS_TRUST_LABEL` | `0x14` (20) |
| `KACS_ACE_TYPE_SYSTEM_ACCESS_FILTER` | `0x15` (21) |

*ACE header `ace_flags` byte — inheritance and audit control.*

| Constant | Value |
|---|---|
| `KACS_ACE_FLAG_OBJECT_INHERIT` | `0x01` (1) |
| `KACS_ACE_FLAG_CONTAINER_INHERIT` | `0x02` (2) |
| `KACS_ACE_FLAG_NO_PROPAGATE_INHERIT` | `0x04` (4) |
| `KACS_ACE_FLAG_INHERIT_ONLY` | `0x08` (8) |
| `KACS_ACE_FLAG_INHERITED` | `0x10` (16) |
| `KACS_ACE_FLAG_SUCCESSFUL_ACCESS` | `0x40` (64) |
| `KACS_ACE_FLAG_FAILED_ACCESS` | `0x80` (128) |

Mandatory-label policy bits — the __le32 access mask of a
KACS_ACE_TYPE_SYSTEM_MANDATORY_LABEL ACE. They control which DACL-
granted rights a caller whose integrity level does not dominate the
object's label (the "up" direction) is denied. Each bit suppresses the
rights mapped from the corresponding generic class; unknown bits MUST be
ignored.

| Constant | Value |
|---|---|
| `KACS_SYSTEM_MANDATORY_LABEL_NO_READ_UP` | `0x00000001` (1) |
| `KACS_SYSTEM_MANDATORY_LABEL_NO_WRITE_UP` | `0x00000002` (2) |
| `KACS_SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP` | `0x00000004` (4) |

Object-ACE body `Flags` field — the __le32 at object-ACE body offset 8,
distinct from the 1-byte `ace_flags` header field above. Indicates which
optional GUIDs the object-ACE body carries.

| Constant | Value |
|---|---|
| `KACS_ACE_OBJECT_TYPE_PRESENT` | `0x00000001` (1) |
| `KACS_ACE_INHERITED_OBJECT_TYPE_PRESENT` | `0x00000002` (2) |

## 3.A.9 SID constants

From `uapi/pkm/sid.h`.

*Largest sub_authority_count a valid SID may declare.*

| Constant | Value |
|---|---|
| `KACS_SID_MAX_SUB_AUTHORITIES` | `15` |

*Encoded byte length of a SID with the given sub-authority count.*

| Constant | Value |
|---|---|
| `KACS_SID_BYTE_LEN(count)` | `(8 + 4 * (count))` |

*SID_AND_ATTRIBUTES "Attributes" bits (MS-DTYP 2.4.4).*

These describe how a group or restricted SID participates in an access
check. MS-DTYP names them for groups; they apply to any
SID_AND_ATTRIBUTES entry.

| Constant | Value |
|---|---|
| `KACS_SID_GROUP_MANDATORY` | `0x00000001` (1) |
| `KACS_SID_GROUP_ENABLED_BY_DEFAULT` | `0x00000002` (2) |
| `KACS_SID_GROUP_ENABLED` | `0x00000004` (4) |
| `KACS_SID_GROUP_OWNER` | `0x00000008` (8) |
| `KACS_SID_GROUP_USE_FOR_DENY_ONLY` | `0x00000010` (16) |
| `KACS_SID_GROUP_INTEGRITY` | `0x00000020` (32) |
| `KACS_SID_GROUP_INTEGRITY_ENABLED` | `0x00000040` (64) |
| `KACS_SID_GROUP_RESOURCE` | `0x20000000` |
| `KACS_SID_GROUP_LOGON_ID` | `0xC0000000` |

## 3.A.10 Tracepoint diagnostic codes

From `uapi/pkm/trace.h`.

*kacs_access_decision reason — why a KACS access hook took the return path it did.*

Emitted by the kacs:kacs_file_access / _file_open / _native_open
_inode_file_access / _inode_permission events. Verdict (allow vs deny)
is a separate signal, read from the `ret` field (0 == allow).

| Constant | Value | Notes |
|---|---|---|
| `KACS_TR_DECISION` | `0` | resolved allow/deny |
| `KACS_TR_BAD_ARGS` | `1` | NULL/zero argument guard |
| `KACS_TR_NO_ISEC` | `2` | inode has no i_security blob |
| `KACS_TR_UNMANAGED` | `3` | superblock mount policy UNMANAGED |
| `KACS_TR_PIP_CONTEXT` | `4` | current PIP context unavailable |
| `KACS_TR_NO_TOKEN` | `5` | no effective subject token |
| `KACS_TR_NO_DENTRY_ALIAS` | `6` | inode has no dentry alias yet |
| `KACS_TR_DELETE_ON_CLOSE_PENDING` | `7` | open of a delete-on-close file |
| `KACS_TR_NATIVE_STAMP` | `8` | native-open granted-access stamp |
| `KACS_TR_NATIVE_ARM` | `9` | native-open delete-on-close arm |
| `KACS_TR_STAMP` | `10` | legacy-open granted-access stamp |
| `KACS_TR_LAZY_DENTRY_RELOOKUP` | `11` | native create lazy re-lookup |
| `KACS_TR_NEGATIVE_AFTER_CREATE` | `12` | negative dentry after create |
| `KACS_TR_CHANGE_NOTIFY_PRIV` | `13` | traverse via CHANGE_NOTIFY priv |
| `KACS_TR_CHANGE_NOTIFY_PRIV_EXHAUSTED` | `14` | CHANGE_NOTIFY priv use exhausted |

*kacs_sd_cache reason — the inode security-descriptor cache outcome.*

The lookup miss codes disambiguate the three cache-absent paths that
were previously an indistinguishable NULL return (no cache / stale
generation missing-but-synthesis-required); the corrupt codes name why a
stored SD was rejected. Emitted by kacs:kacs_sd_cache_lookup / _corrupt.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SDC_HIT` | `0` | current, valid cache present |
| `KACS_SDC_MISS_NONE` | `1` | no cache attached |
| `KACS_SDC_MISS_STALE_GEN` | `2` | cache present but stale generation |
| `KACS_SDC_MISS_NEEDS_SYNTH` | `3` | missing SD requires synthesis |
| `KACS_SDC_CORRUPT_EMPTY_OR_OVERSIZE` | `4` | stored SD zero-length or oversize |
| `KACS_SDC_CORRUPT_VALIDATE_FAIL` | `5` | stored SD failed validation |

kacs_process_access reason — the outcome of a cross-process access
decision (signal, ptrace, scheduler/attribute, prlimit). The reason
distinguishes the paths that all surface as -EACCES: an SD denial, a
PIP-based denial, a denial rescued (or not) by SeDebugPrivilege, and a
PIP-dominance failure. Emitted by kacs:kacs_process_access.

| Constant | Value | Notes |
|---|---|---|
| `KACS_PA_ALLOW` | `0` | access granted |
| `KACS_PA_BAD_ARGS` | `1` | NULL subject/target guard |
| `KACS_PA_NO_TARGET` | `2` | target has no process state/SD |
| `KACS_PA_NO_SD` | `3` | target process SD unavailable |
| `KACS_PA_SD_ERROR` | `4` | SD check failed (non-EACCES) |
| `KACS_PA_PIP_DENIED` | `5` | denied by process-integrity policy |
| `KACS_PA_DEBUG_RESCUE` | `6` | SD denial rescued by SeDebugPrivilege |
| `KACS_PA_DEBUG_DENIED` | `7` | denied; no usable SeDebugPrivilege |
| `KACS_PA_PIP_DOMINANCE` | `8` | caller PIP does not dominate target |

*kacs_exec reason — an exec/bprm credential or PIP transition.*

Distinguishes the uid/gid-change gate outcomes, the exec primary-token
derivation paths and their failures, the exec file integrity-label
lookup failures, and the two commit-time transitions. Verdict is the
`ret` field. Emitted by kacs:kacs_exec.

| Constant | Value | Notes |
|---|---|---|
| `KACS_EXEC_CREDS_ALLOW` | `0` | exec cred transition allowed |
| `KACS_EXEC_BAD_ARGS` | `1` | NULL cred/token guard |
| `KACS_EXEC_ID_CHANGE_NO_TOKEN` | `2` | uid/gid change, no subject token |
| `KACS_EXEC_ID_CHANGE_PRIV_UNSUPPORTED` | `3` | id change + ASSIGN_PRIMARY_TOKEN priv |
| `KACS_EXEC_TOKEN_NPM_DERIVED` | `4` | exec token derived via new-process-min |
| `KACS_EXEC_TOKEN_CLONE` | `5` | exec token via primary clone fallback |
| `KACS_EXEC_NPM_NO_FILE` | `6` | new-process-min needs file, none supplied |
| `KACS_EXEC_NPM_DERIVE_FAIL` | `7` | new_process_min_exec derivation failed |
| `KACS_EXEC_TOKEN_INSTALL_FAIL` | `8` | install token ref on new cred failed |
| `KACS_EXEC_TOKEN_CLONE_FAIL` | `9` | primary token clone returned NULL |
| `KACS_EXEC_INTEGRITY_NO_ISEC` | `10` | exec file inode has no i_security |
| `KACS_EXEC_INTEGRITY_NO_CACHE` | `11` | exec file SD cache absent |
| `KACS_EXEC_INTEGRITY_INVALID_SD` | `12` | exec file cached SD invalid/empty |
| `KACS_EXEC_IMPERSONATION_REVERT_FAIL` | `13` | bprm impersonation revert failed |
| `KACS_EXEC_PIP_COMMITTED` | `14` | pending exec PIP committed at commit |
| `KACS_EXEC_UMH_NOT_TCB` | `15` | usermodehelper exec below PeiosTcb trust |
| `KACS_EXEC_SIGNATURE_UNVERIFIABLE` | `16` | exec refused: signature could not be verified |

kacs_signing reason — code-signature verification outcomes and the
distinct reject reasons of the signing-material probe. Only source enum,
verified flag, PIP tier codes, file length, reason, and ret are recorded
— never key, signature, xattr, or file bytes. Emitted by
kacs:kacs_signing_verify _crypto / _probe.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SIG_UNSIGNED` | `0` | material source NONE (unsigned) |
| `KACS_SIG_BAD_KEY_TABLE` | `1` | key table malformed / bad args |
| `KACS_SIG_NO_KEY_MATCH` | `2` | no key verified the signature |
| `KACS_SIG_VERIFIED` | `3` | a key verified; trust assigned |
| `KACS_SIG_CRYPTO_UNAVAILABLE` | `4` | mldsa65 tfm allocation failed |
| `KACS_SIG_CRYPTO_MISMATCH` | `5` | set-pubkey/verify returned nonzero |
| `KACS_SIG_PROBE_FOUND` | `6` | valid signature material committed |
| `KACS_SIG_ELF_MAGIC_READ` | `7` | failed reading ELF magic |
| `KACS_SIG_ELF_SHORT_EHDR` | `8` | file too short for Elf64_Ehdr |
| `KACS_SIG_ELF_EHDR_READ` | `9` | failed reading ELF header |
| `KACS_SIG_ELF_BAD_IDENT` | `10` | unsupported e_ident class/data/version |
| `KACS_SIG_ELF_BAD_SHTABLE` | `11` | bad shentsize/shstrndx |
| `KACS_SIG_ELF_SHDRS_RANGE` | `12` | section-header table offset/len out of range |
| `KACS_SIG_ELF_SHSTR_READ` | `13` | failed reading shstrtab section header |
| `KACS_SIG_ELF_STRTAB_RANGE` | `14` | shstrtab offset/len out of range |
| `KACS_SIG_ELF_SHDR_READ` | `15` | failed reading a section header |
| `KACS_SIG_ELF_NAME_READ` | `16` | failed reading a section name |
| `KACS_SIG_ELF_BAD_SIG_SECTION` | `17` | sig section wrong type/size/range |
| `KACS_SIG_ELF_BAD_BLOB` | `18` | sig blob read failed or invalid |
| `KACS_SIG_ELF_HASH_FAIL` | `19` | hashing failed for ELF sig |
| `KACS_SIG_XATTR_BAD_BLOB` | `20` | xattr sig blob invalid |
| `KACS_SIG_XATTR_HASH_FAIL` | `21` | hashing failed for xattr sig |
| `KACS_SIG_SIZE_CHANGED` | `22` | file size changed during probe (TOCTOU) |

*kacs_socket reason — the outcome of an AF_UNIX socket SD / impersonation hook.*

Distinguishes the guard, not-applicable, and verdict paths that
otherwise collapse into an indistinguishable -EACCES. Verdict is the
`ret` field. No address, pathname, or SD bytes are recorded.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SOCK_BAD_ARGS` | `0` | NULL/guard argument rejected |
| `KACS_SOCK_NOT_UNIX` | `1` | not AF_UNIX / unsupported type |
| `KACS_SOCK_NO_SECURITY` | `2` | sock has no sk_security blob |
| `KACS_SOCK_NO_TOKEN` | `3` | no effective subject/client token |
| `KACS_SOCK_BAD_LEVEL` | `4` | invalid impersonation level |
| `KACS_SOCK_WRONG_STATE` | `5` | socket state forbids the op |
| `KACS_SOCK_NO_PEER_TOKEN` | `6` | no captured peer token present |
| `KACS_SOCK_PIP_CONTEXT` | `7` | caller PIP context unavailable |
| `KACS_SOCK_SD_DECISION` | `8` | socket-SD check produced verdict |
| `KACS_SOCK_NO_SD` | `9` | no socket SD; allowed without check |
| `KACS_SOCK_HAVE_SD` | `10` | socket SD present; check performed |
| `KACS_SOCK_ALREADY_BOUND` | `11` | socket SD already installed |
| `KACS_SOCK_BIND` | `12` | abstract-socket SD bind result |
| `KACS_SOCK_CONNECT` | `13` | unix_stream_connect result |
| `KACS_SOCK_LEVEL_SET` | `14` | impersonation level updated |
| `KACS_SOCK_OPEN_TOKEN` | `15` | open peer-token fd result |
| `KACS_SOCK_IMPERSONATE` | `16` | impersonate peer result |

*kacs_namespace stage — which sub-decision of a namespace-mutation hook a record describes.*

Single-decision ops report PRIMARY; multi-stage ops (link, rename,
delete fallback) tag each distinct verdict. Verdict is the `ret` field.
Never records a pathname. Emitted by the kacs:kacs_inode_* events.

| Constant | Value | Notes |
|---|---|---|
| `KACS_NS_PRIMARY` | `0` | the op's principal decision |
| `KACS_NS_PARENT_FALLBACK` | `1` | delete: parent DELETE_CHILD fallback |
| `KACS_NS_SOURCE` | `2` | link/rename source-side decision |
| `KACS_NS_DEST` | `3` | link/rename destination-parent add |
| `KACS_NS_DELETE_EXISTING` | `4` | rename: delete pre-existing dest |

kacs_psb reason — which process-security-baseline path an event marks:
mitigation activation (apply) or a W^X / LSV / PIE / prctl-lock
enforcement denial. The ok-vs-deny verdict is read from `ret`. Emitted
by kacs:kacs_psb_*.

| Constant | Value | Notes |
|---|---|---|
| `KACS_PSB_APPLY_OK` | `0` | mitigations applied; result_bits set |
| `KACS_PSB_APPLY_NORMALIZE` | `1` | requested mask bad or unsupported (EINVAL/ENODEV) |
| `KACS_PSB_APPLY_MM_ACQUIRE` | `2` | could not acquire target mm (EACCES) |
| `KACS_PSB_APPLY_CFIF` | `3` | forward-CFI (IBT) activation failed |
| `KACS_PSB_APPLY_SML` | `4` | speculative-mitigation-lock activation failed |
| `KACS_PSB_APPLY_CFIB` | `5` | backward-CFI (shadow stack) activation failed |
| `KACS_PSB_WXP_MMAP` | `6` | W^X blocked a W+X mmap |
| `KACS_PSB_WXP_MPROTECT` | `7` | W^X blocked an mprotect transition |
| `KACS_PSB_WXP_EXISTING_VMA` | `8` | W^X activation blocked by an existing W+X vma |
| `KACS_PSB_LSV_PROBE` | `9` | LSV signing probe of the image failed |
| `KACS_PSB_LSV_VERIFY` | `10` | LSV signature not verified/trusted |
| `KACS_PSB_LSV_PIP_DOMINANCE` | `11` | LSV image PIP does not dominate process PIP |
| `KACS_PSB_PIE_ET_EXEC` | `12` | PIE blocked a non-PIE ET_EXEC image |
| `KACS_PSB_PRCTL_SML` | `13` | prctl blocked by SML lock |
| `KACS_PSB_PRCTL_CFIB` | `14` | prctl blocked by shadow-stack (CFIB) lock |
| `KACS_PSB_PRCTL_PIP` | `15` | prctl set-dumpable blocked by process PIP |

*kacs_token_ioctl cmd — which token-fd ioctl verb a record describes.*

The verdict (allow vs deny) is read from the `ret` field (0 == allow);
the access-mask-gate rejections surface as ret == -EACCES. `token` is an
opaque numeric id (never token bytes). Emitted by kacs:kacs_token_ioctl.

| Constant | Value | Notes |
|---|---|---|
| `KACS_TOK_QUERY` | `0` | KACS_IOC_QUERY |
| `KACS_TOK_ADJUST_PRIVS` | `1` | KACS_IOC_ADJUST_PRIVS |
| `KACS_TOK_ADJUST_GROUPS` | `2` | KACS_IOC_ADJUST_GROUPS |
| `KACS_TOK_DUPLICATE` | `3` | KACS_IOC_DUPLICATE |
| `KACS_TOK_INSTALL` | `4` | KACS_IOC_INSTALL |
| `KACS_TOK_RESTRICT` | `5` | KACS_IOC_RESTRICT |
| `KACS_TOK_LINK` | `6` | KACS_IOC_LINK_TOKENS |
| `KACS_TOK_GET_LINKED` | `7` | KACS_IOC_GET_LINKED_TOKEN |
| `KACS_TOK_IMPERSONATE` | `8` | KACS_IOC_IMPERSONATE |
| `KACS_TOK_ADJUST_DEFAULT` | `9` | KACS_IOC_ADJUST_DEFAULT |
| `KACS_TOK_ADJUST_INTERACTIVITY_SCOPE` | `10` | KACS_IOC_ADJUST_INTERACTIVITY_SCOPE |
| `KACS_TOK_UNKNOWN` | `11` | unrecognised ioctl verb (-ENOTTY) |

*kacs_token_ref reason — a token-fd reference lifecycle transition.*

TO_FD is a token installed into a fresh anon-inode handle; RELEASE is
the handle teardown that drops the token ref; BIND clones a token onto
an existing file; OPEN is the checked/fixed-access open path that clones
the target token. `token` is an opaque numeric id, never token bytes.
Emitted by kacs:kacs_token_ref.

| Constant | Value | Notes |
|---|---|---|
| `KACS_TREF_TO_FD` | `0` | token installed into a new fd (ret == fd) |
| `KACS_TREF_RELEASE` | `1` | token-fd released; ref dropped |
| `KACS_TREF_BIND` | `2` | token cloned + bound onto an existing file |
| `KACS_TREF_OPEN` | `3` | token cloned for a token-open path |

*kacs_logon_session reason — a session/token creation-surface outcome.*

The *_DENIED codes name the privilege-gate rejections (the value); the
plain op codes mark the successful op. Verdict is also in `ret`. No
token/spec bytes are recorded. Emitted by kacs:kacs_logon_session.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SES_CREATE` | `0` | create_logon_session published a session |
| `KACS_SES_CREATE_PRIV_DENIED` | `1` | create_logon_session: TCB privilege gate denied |
| `KACS_SES_DESTROY` | `2` | destroy_empty_logon_session outcome |
| `KACS_SES_DESTROY_PRIV_DENIED` | `3` | destroy: TCB privilege gate denied |
| `KACS_SES_CREATE_TOKEN` | `4` | create_token issued a token fd |
| `KACS_SES_CREATE_TOKEN_PRIV_DENIED` | `5` | create_token: CREATE_TOKEN privilege denied |

kacs_cred reason — a credential-security lifecycle transition: LSM cred
prepare/transfer/alloc/free, explicit token-ref install, the clone-time
primary-token lifecycle (CLONE_THREAD share vs fork deep-copy), and the
project-linux-cred rejection paths. old_token/new_token are opaque token
pointer ids (0 when absent); clone_flags is set only on the clone paths.
Verdict/outcome is the `ret` field. Emitted by kacs:kacs_cred.

| Constant | Value | Notes |
|---|---|---|
| `KACS_CRED_PREPARE` | `0` | cred_prepare token clone |
| `KACS_CRED_TRANSFER` | `1` | cred_transfer token clone |
| `KACS_CRED_ALLOC_BLANK` | `2` | cred_alloc_blank cleared sec |
| `KACS_CRED_FREE` | `3` | cred_free released token/state |
| `KACS_CRED_INSTALL_TOKEN_REF` | `4` | install token ref on a cred |
| `KACS_CRED_CLONE_THREAD_SHARE` | `5` | CLONE_THREAD shares parent primary cred |
| `KACS_CRED_CLONE_FORK_COPY` | `6` | fork deep-copies parent primary token |
| `KACS_CRED_PROJECT_UID0_BLOCKED` | `7` | uid0 projection not allowed by token |
| `KACS_CRED_PROJECT_GROUPS_ALLOC_FAIL` | `8` | groups_alloc failed (ENOMEM) |
| `KACS_CRED_PROJECT_E2BIG` | `9` | supplementary gid count > NGROUPS_MAX |

kacs_setid reason — the KACS gate on a Linux setid projection
(task_fix_setuid / _setgid / _setgroups). Each op has two distinct
denials that otherwise collapse: NO_TOKEN (-EACCES, no effective subject
token) and PRIV_GATE (-EOPNOTSUPP, holder of ASSIGN_PRIMARY_TOKEN
privilege). `flags` is the LSM_SETID_* mask (0 for setgroups). Emitted
by kacs:kacs_setid.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SETID_SETUID_NO_TOKEN` | `0` | setuid gate: no subject token |
| `KACS_SETID_SETUID_PRIV_GATE` | `1` | setuid gate: ASSIGN_PRIMARY priv |
| `KACS_SETID_SETGID_NO_TOKEN` | `2` | setgid gate: no subject token |
| `KACS_SETID_SETGID_PRIV_GATE` | `3` | setgid gate: ASSIGN_PRIMARY priv |
| `KACS_SETID_SETGROUPS_NO_TOKEN` | `4` | setgroups gate: no subject token |
| `KACS_SETID_SETGROUPS_PRIV_GATE` | `5` | setgroups gate: ASSIGN_PRIMARY priv |

*kacs_task reason — a task-security lifecycle transition.*

task_alloc reports a NO_CHILD-mitigation clone block, a process-state
inherit ENOMEM, or success; task_free marks teardown. `process_state` is
an opaque process-state pointer id (0 when absent); `clone_flags` is set
on the alloc paths. Outcome is `ret`. Emitted by kacs:kacs_task.

| Constant | Value | Notes |
|---|---|---|
| `KACS_TASK_ALLOC_NO_CHILD_BLOCKED` | `0` | clone blocked by NO_CHILD mitigation |
| `KACS_TASK_ALLOC_INHERIT_ENOMEM` | `1` | process-state inherit failed (ENOMEM) |
| `KACS_TASK_ALLOC` | `2` | task_alloc completed |
| `KACS_TASK_FREE` | `3` | task_free teardown |

*kacs_primary_install reason — a primary-token / impersonation credential transition.*

Distinguishes the install commit, the user-SID-change process-SD
reallocation and its ENOMEM, the commit_creds apply, the impersonation
override/revert, and the sibling-thread taskwork requeue/failure.
old_primary and new_primary are opaque token identity ids (never token
bytes). Verdict is the `ret` field. Emitted by
kacs:kacs_primary_install.

| Constant | Value | Notes |
|---|---|---|
| `KACS_PRIM_INSTALL_OK` | `0` | primary token install committed |
| `KACS_PRIM_SD_REALLOC` | `1` | user-SID changed; process SD reallocated |
| `KACS_PRIM_SD_ALLOC_FAIL` | `2` | process SD realloc failed (ENOMEM) |
| `KACS_PRIM_APPLY_COMMIT` | `3` | new real creds committed (commit_creds) |
| `KACS_PRIM_IMPERSONATE_INSTALL` | `4` | impersonation token installed (override_creds) |
| `KACS_PRIM_IMPERSONATE_REVERT` | `5` | impersonation reverted (revert_creds) |
| `KACS_PRIM_SIBLING_REQUEUE` | `6` | queued sibling install re-queued on ENOMEM |
| `KACS_PRIM_SIBLING_FAILED` | `7` | queued sibling install failed after apply |

kacs_process_token_open reason — the outcome of opening a process/thread
primary or effective token (kacs_open_process_token / _thread_token and
the proc inspection files). Distinguishes the bad access-mask reject,
the no-target-token path, the process access-check denial, the self vs
cross inspection verdicts, and the successful open.
subject_token/target_token are opaque token identity ids (0 when unknown
at the emit site); access_mask is the requested mask. Verdict is `ret`
(>=0 fd == allow). Emitted by kacs:kacs_process_token_open.

| Constant | Value | Notes |
|---|---|---|
| `KACS_PTO_OPEN_OK` | `0` | token fd opened |
| `KACS_PTO_BAD_ARGS` | `1` | NULL subject/state/task guard |
| `KACS_PTO_NO_TARGET` | `2` | target has no token |
| `KACS_PTO_BAD_ACCESS` | `3` | invalid access mask rejected |
| `KACS_PTO_ACCESS_DENIED` | `4` | process access check denied |
| `KACS_PTO_SELF` | `5` | self-target inspection allowed |
| `KACS_PTO_CROSS` | `6` | cross-process inspection authorized |

*kacs_process_state reason — a process-state / process-SD lifecycle or PIP transition.*

Covers process-state alloc/free, the CLONE_THREAD share vs fork
inheritance split, the no-child clone block, the pending-exec-PIP
stage/commit and dumpable hardening, and the process-SD
alloc/wrap/replace primitives. process_state is an opaque state-object
id (0 when none in scope, e.g. the process-SD primitives);
pip_type/pip_trust carry the PIP tier. `ret` is the outcome (0 == ok).
No token or SD bytes. Emitted by kacs:kacs_process_state.

| Constant | Value | Notes |
|---|---|---|
| `KACS_PST_ALLOC` | `0` | process state allocated |
| `KACS_PST_ALLOC_FAIL` | `1` | process state alloc failed (ENOMEM) |
| `KACS_PST_FREE` | `2` | process state freed (refcount hit 0) |
| `KACS_PST_INHERIT_SHARE` | `3` | CLONE_THREAD: parent state shared |
| `KACS_PST_INHERIT_FORK` | `4` | fork: new state allocated from parent |
| `KACS_PST_EXEC_PIP_STAGE` | `5` | pending exec PIP staged |
| `KACS_PST_EXEC_PIP_COMMIT` | `6` | pending exec PIP committed to state |
| `KACS_PST_DUMPABLE` | `7` | exec dumpable hardened by PIP |
| `KACS_PST_CLONE_BLOCKED_NOCHILD` | `8` | clone blocked by NO_CHILD mitigation |
| `KACS_PST_SD_ALLOC` | `9` | default process SD allocated |
| `KACS_PST_SD_ALLOC_FAIL` | `10` | process/socket SD alloc failed |
| `KACS_PST_SD_WRAP_FAIL` | `11` | process SD wrapper alloc failed (ENOMEM) |
| `KACS_PST_SD_REPLACE` | `12` | process SD replaced on state |
| `KACS_PST_SOCKET_SD_ALLOC` | `13` | default socket SD allocated |

*kacs_mount_policy reason — the outcome of a mount-policy set (TCB-gated) or get.*

SET_OK marks a committed policy change (generation bumped); the guard
codes name the pre-commit rejects that otherwise collapse into a bare
-EINVAL/-EOPNOTSUPP/-EPERM. GET_OK/GET_NO_SECURITY are the snapshot
paths. Verdict is also in `ret`. Emitted by kacs:kacs_mount_policy_set /
_get.

| Constant | Value | Notes |
|---|---|---|
| `KACS_MP_SET_OK` | `0` | policy committed; generation bumped |
| `KACS_MP_BAD_ARGS` | `1` | NULL subject/sb/args guard (EINVAL) |
| `KACS_MP_NO_SECURITY` | `2` | superblock has no s_security (EOPNOTSUPP) |
| `KACS_MP_UNMANAGED` | `3` | magic-derived UNMANAGED; not settable (EOPNOTSUPP) |
| `KACS_MP_VALIDATE` | `4` | mount-policy args validation failed (EINVAL) |
| `KACS_MP_TEMPLATE_INVALID` | `5` | template SD bytes failed validation (EINVAL) |
| `KACS_MP_TCB_DENIED` | `6` | SeTcbPrivilege gate denied (EPERM) |
| `KACS_MP_GET_OK` | `7` | policy snapshot returned |
| `KACS_MP_GET_NO_SECURITY` | `8` | get: no s_security; magic-derived policy returned |
| `KACS_MP_FIXED_POLICY` | `9` | filesystem fixes its policy class (EOPNOTSUPP) |

kacs_sd_syscall target_kind — which SD-bearing object a query/set record
describes, resolved by the get_sd/set_sd syscall target-kind
fallthrough. ACCESS_CHECK tags the AccessCheck ingress events
(kacs_access_check*), whose other scalar fields are 0 at the ingress
boundary.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SDS_KIND_TOKEN` | `0` | token-fd target |
| `KACS_SDS_KIND_FILE` | `1` | file/inode target |
| `KACS_SDS_KIND_PROCESS` | `2` | pidfd process target |
| `KACS_SDS_KIND_PATH` | `3` | path-resolved file target |
| `KACS_SDS_KIND_ACCESS_CHECK` | `4` | AccessCheck ingress (not an SD get/set) |

*kacs_sd_syscall reason — the outcome of an SD query/set core.*

QUERY_OK/SET_OK are the success paths; the remaining codes name the
guard / denial paths that otherwise surface as an indistinguishable
-EINVAL/-EACCES/-EOPNOTSUPP. Verdict is also in `ret`. Emitted by
kacs:kacs_sd_query / _set.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SDS_QUERY_OK` | `0` | SD subset extracted and returned |
| `KACS_SDS_SET_OK` | `1` | SD merged/replaced |
| `KACS_SDS_BAD_ARGS` | `2` | NULL/zero argument guard (EINVAL) |
| `KACS_SDS_UNMANAGED` | `3` | superblock UNMANAGED (EOPNOTSUPP) |
| `KACS_SDS_ACCESS_DENIED` | `4` | SD access check denied (EACCES) |
| `KACS_SDS_NO_SD` | `5` | target has no usable SD (EACCES) |
| `KACS_SDS_RESTORE_BYPASS` | `6` | set via SeRestorePrivilege bypass |
| `KACS_SDS_QUERY_FAIL` | `7` | subset extraction failed after auth |

kacs_access_check reason — the AccessCheck kernel-ingress outcome, above
the closed Slice 15 ABI bridge. OK is a completed ingress; the remaining
codes name the ingress-time rejects (token-eval-context gate, token
resolution, and caap-cache lock acquisition). Verdict is also in `ret`.
Emitted by kacs:kacs_access_check / _list.

| Constant | Value | Notes |
|---|---|---|
| `KACS_ACK_OK` | `0` | ingress dispatched to the ABI bridge |
| `KACS_ACK_EVAL_CONTEXT` | `1` | token-eval-context gate denied (EACCES) |
| `KACS_ACK_TOKEN_RESOLVE` | `2` | token/args resolution failed |
| `KACS_ACK_CAAP_LOCK_FAIL` | `3` | caap-cache lock acquisition failed |

*kacs_file_snapshot op — which snapshot-grant file operation an event marks.*

The allow-vs-deny verdict is read from `ret`; `reason` names why a deny
path was taken. Emitted by kacs:kacs_file_snapshot (file_access.c).

| Constant | Value | Notes |
|---|---|---|
| `KACS_FSOP_ACCESS` | `0` | generic snapshot-grant access check |
| `KACS_FSOP_PERMISSION` | `1` | file_permission hook |
| `KACS_FSOP_IOCTL` | `2` | file ioctl snapshot |
| `KACS_FSOP_LOCK` | `3` | file lock snapshot |
| `KACS_FSOP_FCNTL` | `4` | file fcntl snapshot |
| `KACS_FSOP_TRUNCATE` | `5` | file truncate snapshot |
| `KACS_FSOP_FALLOCATE` | `6` | file fallocate snapshot |
| `KACS_FSOP_MMAP` | `7` | file mmap snapshot |
| `KACS_FSOP_MPROTECT` | `8` | file mprotect snapshot |
| `KACS_FSOP_WRITE_INTENT` | `9` | write-intent snapshot |
| `KACS_FSOP_SYSFS_WRITE_GATE` | `10` | unmanaged sysfs write gate |

*kacs_file_snapshot reason — why a snapshot-grant op took its return path.*

DECISION is the resolved allow/deny (verdict in `ret`); the remaining
codes name the distinct deny causes. Emitted by kacs:kacs_file_snapshot.

| Constant | Value | Notes |
|---|---|---|
| `KACS_FSR_DECISION` | `0` | resolved allow/deny (grant compare) |
| `KACS_FSR_SIGNED_EXEC` | `1` | signed-exec content mutation denied |
| `KACS_FSR_GRANT_DENY` | `2` | granted access lacked required right |
| `KACS_FSR_APPEND_DENY` | `3` | append/write intent lacked write grant |
| `KACS_FSR_UNMANAGED_SYSFS` | `4` | unmanaged fd: sysfs write gate applied |
| `KACS_FSR_AUDIT_EMIT_FAIL` | `5` | continuous-audit emit failed |

*kacs_metadata reason — the file-metadata (getattr/setattr/xattr/getsecurity) decision path.*

DECISION/CONSUME_HIT/BEGIN_BUSY mark the begin/consume decision
lifecycle; the remaining codes name the distinct deny reasons of the
xattr setattr hooks. `op_class` carries the internal
PKM_KACS_METADATA_OP_* value; `matched` is the consume match flag.
Emitted by kacs:kacs_metadata (file_metadata.c).

| Constant | Value | Notes |
|---|---|---|
| `KACS_META_DECISION` | `0` | generic metadata decision |
| `KACS_META_CONSUME_HIT` | `1` | consumed a pre-staged decision |
| `KACS_META_BEGIN_BUSY` | `2` | begin failed: a decision already active |
| `KACS_META_CANONICAL_SD` | `3` | canonical SD xattr access denied |
| `KACS_META_CAPS_XATTR` | `4` | capability xattr mutation denied (EPERM) |
| `KACS_META_ACL` | `5` | POSIX ACL xattr denied |
| `KACS_META_SIGNED_EXEC` | `6` | signed-exec xattr/size mutation denied |
| `KACS_META_BAD_ARGS` | `7` | NULL name / dentry guard |
| `KACS_META_INTERNAL_SD` | `8` | internal SD read/write re-entry allowed |
| `KACS_META_GETSECURITY` | `9` | inode_getsecurity outcome |

kacs_native_open_ext reason — a widening decision inside the native
(kacs_open) create/open machinery. The PREPARE_* codes name the arg-
validation reject buckets of pkm_kacs_prepare_native_open; RESOLVE /
BUILD_CREATED_SD DELETE_ON_CLOSE_ARM name the later stage outcomes
(verdict in `ret`). Emitted by kacs:kacs_native_open_ext
(native_open.c).

| Constant | Value | Notes |
|---|---|---|
| `KACS_NOX_PREPARE_OK` | `0` | prepare accepted the request |
| `KACS_NOX_PREPARE_BAD_FLAGS` | `1` | flags/create_options/__pad rejected |
| `KACS_NOX_PREPARE_BAD_SD_ARGS` | `2` | sd_ptr/sd_len/disposition-sd combo bad |
| `KACS_NOX_PREPARE_BAD_DISPOSITION` | `3` | create_disposition out of range |
| `KACS_NOX_PREPARE_BAD_ACCESS` | `4` | desired-access mask invalid/empty |
| `KACS_NOX_PREPARE_UNSUPPORTED` | `5` | valid but unsupported combination |
| `KACS_NOX_RESOLVE` | `6` | resolve-existing-path outcome |
| `KACS_NOX_BUILD_CREATED_SD` | `7` | build-created-file-SD outcome |
| `KACS_NOX_DELETE_ON_CLOSE_ARM` | `8` | delete-on-close arm outcome |

*kacs_object reason — which object-lifecycle verdict a record marks.*

Only the high-value transitions are traced (pure inode/file/sb
alloc/free are not). `ret` is the outcome (0 == ok). Emitted by
kacs:kacs_object.

| Constant | Value | Notes |
|---|---|---|
| `KACS_OBJ_DELETE_ON_CLOSE_UNLINK` | `0` | file_release delete-on-close unlink attempt |
| `KACS_OBJ_SIGNED_EXEC_PIN` | `1` | inode pinned as signed-exec (immutable) |
| `KACS_OBJ_SIGNED_EXEC_MUTATION_BLOCKED` | `2` | content mutation of a signed-exec-pinned inode denied |

*kacs_securityfs reason — which securityfs endpoint path a record marks.*

The sessions_read codes disambiguate the deny rungs that otherwise
collapse into an errno; open_self / init report the endpoint outcome.
Verdict is `ret`. Emitted by kacs:kacs_securityfs.

| Constant | Value | Notes |
|---|---|---|
| `KACS_SFS_LOGON_SESSIONS_NO_TOKEN` | `0` | sessions read: no effective subject token |
| `KACS_SFS_LOGON_SESSIONS_PIP_CONTEXT` | `1` | sessions read: caller PIP context unavailable |
| `KACS_SFS_LOGON_SESSIONS_ACCESS_CHECK` | `2` | sessions read: rust access check denied |
| `KACS_SFS_OPEN_SELF` | `3` | open of kacs/self self-token file outcome |
| `KACS_SFS_INIT` | `4` | securityfs kacs/ endpoint init outcome |

*kacs_caap reason — which CAAP policy-cache path a record marks.*

SET carries the post-set cache_len (an insert grows it; an evict/replace
may shrink it); INIT/DESTROY are cache lifecycle; TCB_GATE is the
SeTcbPrivilege gate deny. Emitted by kacs:kacs_caap. No SID or spec
bytes — lengths only.

| Constant | Value | Notes |
|---|---|---|
| `KACS_CAAP_TCB_GATE` | `0` | SeTcbPrivilege gate denied the caller |
| `KACS_CAAP_SET` | `1` | cache set (insert/evict); cache_len is post-set count |
| `KACS_CAAP_INIT` | `2` | CAAP cache created |
| `KACS_CAAP_DESTROY` | `3` | CAAP cache destroyed |

kacs_capability reason — the capability->privilege gate verdicts and the
capability LSM-hook outcomes. ALLOW_GRANT is an auto-granted allow-cap;
HARD_DENY is the SETPCAP/SETFCAP/MAC_OVERRIDE hard block;
PRIV_NOT_ENABLED USE_MARK_FAIL are the mapped-privilege gate failures;
CAPSET / PRCTL_GUARD CAPABLE / CAPGET report the corresponding hook
outcome. Emitted by kacs:kacs_capability. Verdict is `ret`.

| Constant | Value | Notes |
|---|---|---|
| `KACS_CAP_ALLOW_GRANT` | `0` | allow-cap auto-granted (no privilege needed) |
| `KACS_CAP_HARD_DENY` | `1` | SETPCAP/SETFCAP/MAC_OVERRIDE hard-denied |
| `KACS_CAP_PRIV_NOT_ENABLED` | `2` | mapped privilege not enabled on token |
| `KACS_CAP_USE_MARK_FAIL` | `3` | privilege use-mark failed |
| `KACS_CAP_CAPSET` | `4` | capset core outcome |
| `KACS_CAP_PRCTL_GUARD` | `5` | prctl capability-guard outcome |
| `KACS_CAP_CAPABLE` | `6` | capable() hook guard-deny outcome |
| `KACS_CAP_CAPGET` | `7` | capget for-task outcome |

kacs_privilege reason — the require_enabled_privilege gate rungs plus
two standalone privilege-path markers. NULL_OR_ZERO is a null-
token/zero-mask guard; NOT_ENABLED / USE_MARK_FAIL are the gate
failures; CHANGE_NOTIFY marks the open_by_handle_at
SeChangeNotifyPrivilege check outcome; RCU_ENOMEM_ FALLBACK marks the
deferred-free ENOMEM synchronize_rcu fallback. Emitted by
kacs:kacs_privilege. Verdict is `ret`.

| Constant | Value | Notes |
|---|---|---|
| `KACS_PRIV_NULL_OR_ZERO` | `0` | null token or zero privilege mask |
| `KACS_PRIV_NOT_ENABLED` | `1` | privilege not enabled on token |
| `KACS_PRIV_USE_MARK_FAIL` | `2` | privilege use-mark failed |
| `KACS_PRIV_CHANGE_NOTIFY` | `3` | open_by_handle_at CHANGE_NOTIFY gate outcome |
| `KACS_PRIV_RCU_ENOMEM_FALLBACK` | `4` | deferred-free kmalloc failed; sync-rcu fallback |

*kacs_tlp reason — the trusted-launch-path decisions.*

CHECK_PATH marks a no-prefix-match executable-transition deny (path_len
+ prefix_count only, NEVER path or prefix bytes); REPLACE marks a
prefix-table replacement. Emitted by kacs:kacs_tlp. Verdict is `ret`.

| Constant | Value | Notes |
|---|---|---|
| `KACS_TLP_CHECK_PATH` | `0` | executable transition denied: no prefix match |
| `KACS_TLP_REPLACE` | `1` | TLP prefix table replaced |

---

# Appendix 3.B Departures from MS-DTYP

_Peios / Advanced Peios / PKM / KACS_

> Where KACS departs from MS-DTYP despite using its binary formats, and which features are handled elsewhere rather than here.

KACS uses the binary formats MS-DTYP specifies, so a descriptor
authored by a Windows domain controller and replicated through Samba
is evaluated without translation. PCDS specifies those formats
normatively.

Evaluator behaviour is a separate question. Given the same token,
descriptor and desired mask, KACS generally reaches the same decision
MS-DTYP describes — which is what makes policy authored in an AD
environment behave predictably here — but it departs deliberately in
the following places.

| Area | Departure | Why |
|---|---|---|
| Conditional ACE `@Local.` | Resolved from an AccessCheck parameter rather than a token field | The context is per-call and varies between checks. |
| Virtual groups in expressions | `Member_of({S-1-3-4})` returns true for the owner | Keeps the SID matcher and the expression evaluator semantically consistent. |
| INT64/UINT64 promotion | Relational operators promote between the two | Without promotion, `UINT64` claims cannot be used in conditions at all. |
| `Member_of` filtering | Filtered by ACE polarity, so deny-only groups do not satisfy allow-ACE conditions | Consistent with deny-only group semantics everywhere else. |
| `Exists` scope | Extended to all four attribute namespaces | No reason to restrict existence tests to Local and Resource. |
| ACE mask mapping | ACE masks are mapped through GenericMapping at evaluation time | Required for `GENERIC_ALL` in central access policy recovery ACEs (§3.8.8). |
| `MAXIMUM_ALLOWED` | First-writer-wins for targeted *and* maximum-allowed requests | Eliminates disagreement between "what can I do?" and "can I do this?" on a non-canonically ordered DACL. |
| Zero desired mask | Succeeds rather than returning access denied | "Asked for nothing, got nothing" is a valid answer. |
| Alarm ACEs | Repurposed for continuous per-operation auditing (§3.8.9) | Reserved but never implemented in the reference model. |
| Multiple scoped policy ACEs | Several permitted per SACL | AND semantics make composition safe. |
| Mandatory policy mutability | `mandatory_policy` is immutable on the token (§3.2.2) | A mutable policy reduces MIC to advisory. |
| Impersonation integrity ceiling | Enforced unconditionally; `SeImpersonatePrivilege` does not bypass it (§3.5.2) | MIC is a real boundary precisely because the mandatory policy is immutable. |
| Impersonation origin check | Dropped | Eliminates hidden impersonation paths. |
| PIP determination | Kernel-only, from the binary signature, with no parent input (§3.3.2) | One input, one answer, no ambiguity. |
| Object type list validation | Duplicate GUIDs and level gaps rejected (§3.8.5) | Prevents node lookup returning the wrong node and propagation becoming undefined. |
| Composite equality | Element-wise ordered comparison | Never over-grants. |

## 3.B.1 Features handled elsewhere

Several capabilities relevant to a complete security posture are not
KACS's, and are named here so their absence is not mistaken for a gap.
Kerberos and NTLM authentication, S4U, and credential storage and
protection belong to authd, as do Resource-Based Constrained
Delegation and Authentication Policies and Silos through the KDC.
Active Directory replication is Samba's. Group Policy distribution
goes through the registry and roles. Network share permissions belong
to the Samba SMB layer. An Encrypting File System is a future service.

---

# Appendix 3.C Audit Event Schemas

_Peios / Advanced Peios / PKM / KACS_

> The audit records KACS emits through KMES — the event families, their shared payload records, and how they are delivered.

KACS emits its audit records through KMES with origin class 2 (§2.2).
Each event's payload is a msgpack map; the shared `subject` and
`process` sub-maps are attached at emission time from the resolved
call context rather than by the evaluation pipeline (§3.8.9).

This appendix covers which events KACS emits and from where. The
field-by-field payload schemas are in the
[Peios Events Index](/peios/using-peios/events/kernel-access-events/access-audit.md), which is
canonical for them.

## 3.C.1 Event families

| Event type | Emitted by |
|---|---|
| `access-audit` | The SACL walk, and token audit-policy forcing. |
| `continuous-audit` | Enforcement points, per operation, against a handle's continuous audit mask. |
| `privilege-use` | Privilege-use auditing, for the five AccessCheck-influencing privileges. |
| `caap-policy-diagnostic` | A CAAP rule SACL error, or a staged-versus-effective mismatch. |
| `logon-session-destroyed` | LogonSession teardown (§3.2.7). |
| `corrupt-sd` | A descriptor xattr that exists but fails structural validation (§3.9.5). |
| `STRATAFS_COPY_UP` | StrataFS copy-up lifecycle and failure (§3.9.7). |
| `STRATAFS_MUTATION_REFUSED` | A StrataFS arrangement refusal. |

The `privilege` field of a `privilege-use` event carries a canonical
name, and only five are representable — `SeSecurityPrivilege`,
`SeTakeOwnershipPrivilege`, `SeBackupPrivilege`, `SeRestorePrivilege`
and `SeRelabelPrivilege`. Any other bit fails the encoder closed
rather than emitting an unnamed privilege, which is consistent with
those being the only five that can produce such an event at all
(§3.4.1).

`continuous-audit` carries an `operation` naming the enforcement
point: `file.access`, `file.mmap`, `file.mprotect`, `file.permission`,
`file.write`, `file.ioctl`, `file.lock`, `file.fcntl`, `file.truncate`
and `file.fallocate`. Its `object_context` field is always nil.

## 3.C.2 Delivery

Audit and privilege-use events are delivered **before** any result is
written back to the caller, and a delivery failure fails the syscall
with `EIO` or `EOPNOTSUPP`. An audit event cannot be suppressed by
handing the call a bad output pointer.

Three emissions are best-effort by contrast, and drop silently rather
than failing the operation that caused them:
`logon-session-destroyed` where the authentication package name is not
valid UTF-8; the two StrataFS events on an allocation failure or an
over-long operation string; and any self-emitted payload that would
exceed its encoding buffer.

The transport itself — ring buffer delivery, buffering and drop
accounting — is KMES's (§2.5, §2.7).

---

# Appendix 3.D KACS ABI Notes

_Peios / Advanced Peios / PKM / KACS_

> What the KACS ABI tables cannot say for themselves — token query payload shapes, the specification names that differ from the headers, what is documented elsewhere, and the kernel configuration.

§3.A is generated from `pkm/uapi/pkm/` and holds only what a compiler
can measure. This appendix holds the rest: the two ACE types that have
a constant and no behaviour, the payload shapes behind the token query
classes, the specification spellings a reader may arrive holding, what
is deliberately documented elsewhere, and the kernel configuration
KACS is built by.

The split is structural rather than editorial. `gen-kacs-abi.py`
overwrites §3.A wholesale on every run, so anything written there is
lost the next time the ABI changes — which is exactly what happened to
two sections of this one before they were moved here.

## 3.D.1 ACE types with no evaluator behaviour

Two of the ACE type constants in §3.A have a constant and nothing
behind it. The ACE parser in `kacs-core` dispatches on 0x00–0x03,
0x05–0x14 and classifies every other value as opaque, so an ACE of type
0x04 or 0x15 is skipped during evaluation and written back
byte-for-byte on serialisation. The constants exist so that a decoder
can put a name to the byte. libpeios' SDDL codec does, printing 0x15 as
`SYSTEM_ACCESS_FILTER`; the `sd` utility does not, and renders both as
`OTHER(0x04)` and `OTHER(0x15)`. PCDS §5.4 records the same state
normatively.

## 3.D.2 Token query payloads

The class numbers come from the header and are tabulated in §3.A;
these are the payloads each one returns. Sizes are in bytes; a variable-length payload uses
the shapes below. An invalid class returns `EINVAL`.

Two repeating shapes appear throughout. A **SID array** is
`[count:u32le]` followed by `count` entries of
`[sid_len:u32le][sid_bytes][attributes:u32le]`, and reports a count of
zero when the array is empty rather than an empty payload. A **claims
array** is `[count:u32le]` followed by `count` entries of
`[entry_len:u32le][entry_bytes]`. A bare SID is the SID bytes alone,
and an absent optional SID or ACL is zero bytes.

| Class | Payload |
|---|---|
| `USER` | Bare SID. |
| `GROUPS` | SID array. |
| `PRIVILEGES` | 32 bytes: present, enabled, enabled-by-default and used, four `u64` in that order. |
| `TYPE` | `u32`, 4 bytes. |
| `INTEGRITY_LEVEL` | The mandatory-label SID `S-1-16-<level>`, 12 bytes. |
| `OWNER` | Bare SID, resolved through the owner index: 0 is the user SID, N is `groups[N-1]`. |
| `PRIMARY_GROUP` | Bare SID, resolved the same way. |
| `INTERACTIVITY_SCOPE` | `u32`, 4 bytes. |
| `RESTRICTED_SIDS` | SID array; count 0 on an unrestricted token. |
| `SOURCE` | 16 bytes: an 8-byte name followed by a `u64` LUID. |
| `STATISTICS` | 40 bytes: token id, LogonSession id, modified id, token type, a reserved zero, and expiration. |
| `ORIGIN` | `u64`, 8 bytes. |
| `ELEVATION_TYPE` | `u32`, 4 bytes. |
| `DEVICE_GROUPS` | SID array. |
| `APPCONTAINER_SID` | Bare SID; empty when the token is unconfined. |
| `CAPABILITIES` | SID array. |
| `MANDATORY_POLICY` | `u32`, 4 bytes. |
| `LOGON_TYPE` | `u32`, 4 bytes, read from the LogonSession. |
| `LOGON_SID` | Bare SID, derived from the LogonSession id. |
| `DEFAULT_DACL` | Binary ACL; empty when none is set. |
| `IMPERSONATION_LEVEL` | `u32`, 4 bytes. |
| `USER_CLAIMS` | Claims array. |
| `DEVICE_CLAIMS` | Claims array. |
| `PROJECTED_SUPPLEMENTARY_GIDS` | `[count:u32le]` followed by `count` `u32` GIDs. |

Nine token fields have no query class at all: `created_at`,
`token_guid`, `audit_policy`, `write_restricted`, `user_deny_only`,
`isolation_boundary`, `confinement_exempt`, the projected UID and GID
— only the supplementary GIDs are reportable —
`restricted_device_groups`, and the LCS registry credentials.

## 3.D.3 Names that differ from the specifications

This manual uses the names `uapi/pkm/` declares, and the generated
tables of §3.A are authoritative for them. A reader may instead arrive
holding the name PCDS uses, which is MS-DTYP's — a legitimate spelling,
not an obsolete one, and the one a third party implementing PCDS will
have. This table maps those onto the headers.

| PCDS / MS-DTYP | uapi name |
|---|---|
| `ACCESS_ALLOWED_ACE_TYPE`, `SYSTEM_AUDIT_ACE_TYPE`, ... | `KACS_ACE_TYPE_ACCESS_ALLOWED`, `KACS_ACE_TYPE_SYSTEM_AUDIT`, ... (the qualifier moves to the front) |
| `KACS_REAL_TOKEN` | `KACS_TOKEN_OPEN_REAL` |
| `KACS_LEVEL_*` | `KACS_IMLEVEL_*` |
| `KACS_FILE_SUPERSEDE`, `_OPEN`, ... | `KACS_DISPOSITION_*` |
| `OWNER_SECURITY_INFORMATION`, ... | `KACS_SECINFO_*` |
| `SE_PRIVILEGE_ENABLED` / `_REMOVED` | `KACS_PRIVILEGE_ATTR_ENABLED` / `_REMOVED` |
| `KACS_PRIV_RESET_ALL_DEFAULTS` | `KACS_PRIVILEGE_RESET_ALL_DEFAULTS` |
| `KACS_RESTRICT_WRITE_RESTRICTED` | `KACS_TOKEN_RESTRICT_WRITE_RESTRICTED` |
| `SE_GROUP_*` | `KACS_SID_GROUP_*` |
| `TOKEN_CLASS_*` | `KACS_TOKEN_CLASS_*` |

The PIP tiers have no public names at all. The Protected type (512)
and the `PeiosTcb` trust level (8192) exist only as kernel-private
constants, and nothing in `uapi/pkm/` defines None, Protected or
Isolated. A program reasoning about tiers compares the numbers
(§3.7).

## 3.D.4 What is not here

Required rights, error codes and validation rules are properties of
the implementation rather than of the headers, so they are documented
with the operations themselves: token rights and the per-ioctl
requirements in §3.2.8, the file rights in §3.9, the process rights
in §3.3.3, and the privileges in §3.4.2.

Two neighbouring ABIs are generated or documented separately.
`uapi/pkm/trace.h` is a versioned, append-only ABI of tracepoint
reason, operation and state codes intended for tooling.
`uapi/pkm/kmes.h` and `uapi/pkm/lcs.h` belong to their own chapters.

## 3.D.5 Build configuration

`CONFIG_SECURITY_PKM=y` and `CONFIG_RUST=y` are required, as are
`CONFIG_STRICT_DEVMEM=y` and `CONFIG_MODULE_SIG_FORCE=y` -- the last
two enforced at initialisation rather than only at build (§3.7).
`CONFIG_SECURITY_SELINUX`, `_APPARMOR`, `_SMACK` and `_TOMOYO` are
refused by Kconfig dependency; `CONFIG_BPF_LSM` is refused only at
runtime, so a kernel enabling both configures and builds and then
fails to initialise. `CONFIG_LSM` is never parsed.

Two further symbols gate large bodies of code:
`CONFIG_SECURITY_PKM_KUNIT`, which compiles in the test harness and,
in the signing path, a different and publicly known verification key
(§3.6); and `CONFIG_STRATAFS_FS`, without which the copy-up API of
§3.9.7 is inert.

---

# 4.1 Overview

_Peios / Advanced Peios / PKM / stratafs_

> stratafs presents an ordered set of existing directories as one tree — where it sits, the model it follows, and what it delegates.

stratafs presents an ordered set of existing directories — its
**strata** — as one merged directory tree. Each stratum is an ordinary
directory on an ordinary filesystem, owned and written by whatever
agent owns it, with no coordination with stratafs and no notification
to it. The merged view reflects changes to any stratum without a
remount.

It is a stacking filesystem in the strict sense: it stores no file
data, no directory entries, and no security descriptors. Every object
reachable through a stratafs mount is a real object on a real stratum,
and every data and metadata operation is performed against that object.
stratafs inodes carry no address-space operations, so there is no
second page cache to keep coherent; reads, writes, mappings and splices
are forwarded to a backing file opened on the provider.

Unlike overlayfs, stratafs has **no whiteouts and no opaque-directory
markers**. There is no mechanism anywhere in the filesystem for
recording that a name should be absent. That single omission accounts
for most of what is unusual about it: removing a name can leave the
name visible, some names cannot be removed at all, and several
operations that succeed on an ordinary filesystem are refused here
rather than faked. §4.8 collects the consequences.

## 4.1.1 Where it sits

stratafs is not part of PKM. It is staged into the kernel tree as
`fs/stratafs`, built by `CONFIG_STRATAFS_FS` — a boolean option, so it
is linked into `vmlinux` — and registered by an `fs_initcall`. The
option depends on `CONFIG_SECURITY_PKM`, because stratafs reaches KACS
for every access decision and for the whole of the copy-up context.
That reach is through `<linux/kacs_stratafs.h>`, a kernel-private
header staged into `include/linux` whose symbols are deliberately not
exported to modules: there is no userspace surface to the interface
between the two, and no way for anything but the in-tree filesystem to
enter it.

The filesystem type registers under the name `stratafs`, with the
superblock magic `0x53545241` — ASCII `STRA` — and the flags
`FS_USERNS_MOUNT` and `FS_RENAME_DOES_D_MOVE`. It takes no device;
superblocks come from `get_tree_nodev`, so the device identifier
reported for every object in a mount is an anonymous one belonging to
the mount rather than to any stratum.

A small part of the filesystem is written in Rust. The crate
`stratafs-core` is staged into the kernel alongside PKM's own Rust
cores and holds three pure, allocation-free decisions: validating the
stack-wide flag rules, selecting the provider from a presence bitmap,
and routing one modifying operation. Everything else — the VFS glue,
resolution, enumeration, copy-up — is C.

## 4.1.2 The model

A mount is defined by its **stratum stack**: an ordered list of strata,
highest-precedence first, fixed for the life of the mount. A stratum is
identified by its **path**, not by the directory that path resolved to
at mount time, which is what allows a package transaction to replace a
whole stratum by renaming trees around underneath a live mount.

For a given name in a given directory, the **provider** is the
highest-precedence stratum that holds it. A name whose provider is a
directory, and which lower strata also hold as a directory, resolves to
a **merged directory** whose entries are the union of theirs. A name
whose provider is anything else masks every lower entry of that name
completely, subtree and all.

At most one stratum carries the `create` flag. That **create stratum**
receives newly created objects and is the destination of **copy-up** —
the replication of an object into the create stratum so that a
modification can be applied without touching the stratum that provides
it. A stack may have no create stratum, in which case nothing can be
created and nothing copied up.

Routing a modification is a decision about strata alone. It happens
when the modifying operation is performed, never at open, and it never
consults the caller: by the time an operation reaches stratafs, KACS
has already decided the caller was entitled to perform it. §4.5.1 sets
out why no other formulation is implementable.

## 4.1.3 What it delegates

stratafs holds no security descriptors, so it makes no access
decisions of its own about the objects it presents. The descriptor
evaluated for an operation is the one on the object the operation will
be performed against, and KACS reads it through the ordinary
extended-attribute path, which the stacking layer forwards down to the
provider. A stratafs mount cannot grant access its provider stratum
would refuse.

A merged directory is the exception, because it stands for several
real directories with several descriptors and forwarding yields only
the provider's. Those checks stratafs performs itself, against every
participating directory, requiring all of them to succeed (§4.6.2).

Copy-up is the other exception, in the opposite direction. It is
machinery serving an operation that was already authorised, not an
operation a caller requests, so it must introduce no new checks against
whichever task happens to execute it. KACS provides a kernel-internal
copy-up context for exactly this, described in full at §3.9.7; §4.6.3
covers the stratafs half.

## 4.1.4 This chapter

§4.2 covers the stack, the mount options that define it, what a
mounter must be entitled to, and absent strata. §4.3 covers resolution:
providers, merging, type conflicts, and enumeration. §4.4 covers
coherency — how uncoordinated change is observed, and the inode
identity presented over it. §4.5 covers mutation: routing, copy-up,
creation, removal, rename, links, locking and durability. §4.6 covers
the security seam with KACS, and §4.7 the one interface stratafs
synthesises for userspace. §4.8 collects the failure modes, including
the divergences from ordinary filesystem behaviour that follow from
having no whiteouts, and §4.A the constants.

---

# 4.2.1 The Stratum Stack

_Peios / Advanced Peios / PKM / stratafs / Strata and Mounting_

> A mount is its stratum stack — an ordered, non-empty list fixed for the mount's life — plus the resolution context and the create stratum.

A mount is defined by its stratum stack: an ordered, non-empty list of
strata, highest-precedence first. The stack is fixed for the life of
the mount. Nothing reorders it, and precedence never varies by path, by
caller, or by operation.

Index 0 is the highest-precedence stratum. That convention runs all the
way through: strata are stored in a fixed array in mount-option order,
presence across the stack is a `u64` bitmap indexed by the same
position, and selecting the provider is the trailing-zero count of that
bitmap — the lowest set index, which is the highest-precedence stratum
that holds the name.

The array is `STRATAFS_MAX_STRATA` entries, which is **16**. A stack
longer than that is refused at parse time. Absence never renumbers
anything: an absent stratum simply has its bit clear, keeping its slot
and its precedence.

## 4.2.1.1 A stratum is a path

What is stored for each stratum is a string and a flag word — nothing
else. No reference is held on a stratum's directory, and no resolution
result is retained across operations. Every time a name is resolved,
the stratum's path string is joined with the relative path of the name
and walked from scratch.

This is what makes a stratum follow a wholesale replacement. A package
transaction that renames the old tree aside and the new tree into place
leaves the original directory object intact and referenced by anything
that held it; a stratum defined as that object would go on serving the
replaced tree. Defined as a path, it follows.

The cost is that every lookup does the walk. §4.4.2 covers what that
means in practice, because the implementation does not cache
resolutions at all.

## 4.2.1.2 The resolution context

Stratum paths are joined absolute and walked from a root captured when
the mount was created — the creating process's own filesystem root —
under credentials captured at the same moment. Both are pinned for the
life of the mount and released only when the superblock is freed.

The credential override matters as much as the root. A stratum path is
resolved with the mounter's credentials, not with those of whatever
process later touches the mount, so a caller in a different mount
namespace cannot shift what a stratum denotes, and the paths reported
by the origin attribute (§4.7) always name the same things.

Symbolic links and mounts along a stratum's path are followed as they
stand at the moment of resolution, since the walk is an ordinary
`filename_lookup` with no restricting flags. A mount established inside
a stratum is therefore part of that stratum's tree, and one stratum can
span several filesystems — which §4.4.3 has to account for when
deriving inode numbers.

## 4.2.1.3 What a stratum's filesystem must provide

Nothing beyond ordinary directory and file operations. stratafs probes
no capability at mount time: it checks only that each stratum path
resolves, names a directory, is not a duplicate of another stratum, and
does not push the composed stack past the kernel's maximum stacking
depth. There is no test for a usable directory version value, because
nothing in the implementation would use one.

## 4.2.1.4 Flags

Each stratum carries zero or more flags, declared with it in the mount
options and stored as a bit field.

| Flag | Bit | Meaning |
|---|---|---|
| `create` | `STRATAFS_F_CREATE` | This stratum receives newly created objects and is the destination of copy-up. |
| `ro` | `STRATAFS_F_RO` | stratafs does not modify this stratum. |
| `am` | `STRATAFS_F_AM` | This stratum's directory may be absent. |

The stack-wide rules are decided in Rust, in `stratafs-core`: the stack
must be non-empty, must not exceed 16 strata, must contain no
unrecognised flag bit, must not carry `create` twice, and must not
carry `create` and `ro` on one stratum. The crate distinguishes five
error cases, but the C boundary collapses all of them to `EINVAL`, so
the distinction is not observable to a caller.

### 4.2.1.4.1 `create`

At most one stratum carries `create`, and a stack may carry none, in
which case `create_index` is `-1` and every creation and every copy-up
is refused with `EROFS`. Modification of an object provided by a
stratum that accepts modification is unaffected, because routing tests
the provider before it consults the create stratum at all (§4.5.1).

`create` does not mean "writable", and it is not the only stratum
stratafs writes to. It designates where objects that do not yet exist
in any stratum are created, and where an object is copied when its
provider will not accept a modification. A stratum carrying neither
`create` nor `ro` is modified in place, and any number of such strata
may sit above the create stratum, below it, or both.

### 4.2.1.4.2 `ro`

`ro` is a stratafs-level assertion, independent of whether the
stratum's filesystem is itself read-only. It is one of three terms in
the predicate that decides whether a stratum **accepts modification**
of an object it provides:

- the stratum does not carry `ro`;
- the provider's mount is not read-only;
- the provider's inode is not marked immutable.

The predicate takes only the superblock, the stratum index, and the
provider path. It reads no credentials, consults no security
descriptor, and calls into KACS not at all — which is what stops a
caller who has been *refused* write access from provoking a copy-up
(§4.5.1).

Note the third term is specifically the immutable inode flag. A file
that is unwritable by its mode bits is not excluded by this predicate;
the write is routed in place and the underlying filesystem refuses it.

### 4.2.1.4.3 `am`

Without `am`, a stratum's directory must exist when the mount is
created, and a mount whose stratum directory is absent fails with
`ENOENT`. With `am`, an absent directory is accepted at mount time.

The flag governs mount time only. At runtime the resolver never
consults it: a stratum whose directory has gone is skipped exactly the
same way whether or not it carries `am`. §4.2.4 covers what absence
means once the mount is live.

---

# 4.2.2 Mount Options

_Peios / Advanced Peios / PKM / stratafs / Strata and Mounting_

> The single filesystem-specific mount parameter stratafs registers, its escaping and parse failures, generic flags, and remount.

stratafs registers exactly one filesystem-specific mount parameter,
`strata`, and rejects every other name. There is no option to select a
security descriptor, an access-check behaviour, an inode-numbering
scheme, or a caching mode.

```
strata=<stratum>[:<stratum>]...
<stratum> := <path>[+<flag>]...
<flag>    := create | ro | am
```

Strata are separated by `:` and given highest-precedence first. Each
stratum is an absolute path, optionally followed by flags, each
introduced by `+`.

```
strata=/system/retc:/lcl/etc+create:/usr/etc+ro
```

`:` separates strata rather than `,` because a mount options string is
itself comma-separated when passed through the legacy mount interface,
which would otherwise split the value.

## 4.2.2.1 Escaping

Within a path, a literal `:`, `+`, `,` or `\` is escaped by a preceding
`\`. Those four characters are the entire escapable set; the escaped
byte is stored literally, and the backslash is consumed.

Because the legacy `mount(2)` data is one comma-separated string,
stratafs replaces the VFS's monolithic option splitter with one that
honours backslash escapes, so that an escaped comma inside a stratum
path survives the split rather than being read as an option boundary.

## 4.2.2.2 Parse failures

Every malformed value is refused with `EINVAL`. The parser consumes the
whole string and errors on any byte it cannot classify; there is no
skip-and-continue path, so nothing it does not understand is silently
ignored.

| Condition | |
|---|---|
| The `strata=` option is absent | There is no default stack |
| Its value is empty | |
| An element between two separators is empty, or the value begins or ends with a separator | |
| A path is not absolute | Tested on the raw first byte, which is exact since `/` is not escapable |
| A path is empty after unescaping | Defensive; the absolute-path test already guarantees one byte |
| An unescaped `,` appears in a path, or inside a flag token | The option string is comma-separated at the outer level |
| A `\` appears at the end of the value, or before a character that is not `:`, `+`, `,` or `\` | A dangling or meaningless escape |
| A `+` is followed by no flag, or by an unrecognised one | Flag names are matched by exact length and content |
| The same flag appears more than once on one stratum | |
| More than 16 strata | The array bound is reached mid-parse |
| `strata=` appears twice in one option string | |

Two paths return `ENOMEM` rather than `EINVAL`, both allocation
failures during parsing. Nothing bounds a stratum path at parse time:
an over-long one is accepted here and fails later with `ENAMETOOLONG`
when the joined path exceeds `PATH_MAX` during a resolution.

The stack-wide conditions — an empty stack, two `create` strata, a
stratum carrying both `create` and `ro` — are checked separately, after
parsing and before any path is resolved. They depend on nothing but the
option string, so they are reported whatever the caller's access
(§4.2.3).

## 4.2.2.3 Generic mount flags

Generic flags apply as they do to any filesystem, with one addition. A
stratafs mount may be mounted read-only, and the superblock's read-only
state is the **first** term of the routing decision, short-circuiting
before the provider or the create stratum is considered at all. It
therefore refuses every mutation with `EROFS` regardless of the stratum
stack, and is both independent of and stricter than a stack with no
create stratum.

Locking and synchronising are unaffected. Neither modifies an object,
neither consults the superblock's read-only state, and a reader of a
merged tree may need both.

## 4.2.2.4 Remount

A remount may alter generic mount flags. It may not alter the stack:
any remount that supplies `strata=` at all is refused with `EINVAL`.

The check is on the presence of the parameter rather than on its value,
so a remount that replays the current stack byte-for-byte is refused
too — which matters, because that is what a tool reconstructing options
from the mount table will do.

## 4.2.2.5 The mount table

The stack is reported in the filesystem options the kernel exposes for
mounts, so it is discoverable by anything that can read the mount
table, which on Linux is unprivileged. What is stored for this purpose
is the caller's own option string, kept verbatim at parse time: nothing
is abbreviated, no stratum is omitted, no path is canonicalised, and an
absent stratum is reported like any other, because the string is fixed
at mount and never filtered by what currently exists.

The filesystem type is reported as `stratafs`.

The value is emitted through the kernel's `seq_show_option`, which
applies its own escaping — octal for `,`, `\`, and whitespace — on top
of the escaping the value already carries. For a path containing none
of `: + , \` or whitespace, which is the ordinary case, the reported
value is byte-identical to what was supplied. For a path containing any
of them it is not reconstructable: a stored `\:` is re-escaped to
`\134:`, which reads back as a dangling escape. This is tracked as a
defect.

---

# 4.2.3 Mount Admission

_Peios / Advanced Peios / PKM / stratafs / Strata and Mounting_

> What a caller must be entitled to and what a configuration must satisfy — evaluation order, loop detection, and mount immutability.

A stratafs mount is configured entirely at mount time. This section
covers what the caller must be entitled to, the conditions a
configuration has to satisfy, and the order in which those conditions
are evaluated.

## 4.2.3.1 Entitlement

Establishing a mount with no create stratum takes no privilege of its
own. The filesystem type carries `FS_USERNS_MOUNT`, so an unprivileged
mount in a user namespace is permitted; what the caller needs is only
the access that resolving the stratum paths already requires, which
falls out of the resolution itself.

A stack that carries `create` is different. Copy-up is authorised by
the outer handle and deliberately requires no add-entry right on the
create stratum (§4.6.2), so the mount configuration carries authority
to materialise names in a real directory outside the mount. The caller
establishing such a stack must be operating in the **initial** user
namespace and hold `CAP_SYS_ADMIN` there; anything else fails with
`EPERM`.

The test is made in `get_tree`, before the tree is built and therefore
before any stratum path is resolved, and it is stricter than requiring
the capability alone: the credential's own user namespace must *be* the
initial one, not merely a namespace in which the capability resolves.
This is the closure the KACS copy-up context depends on. The check is
made once, when the immutable stack is established, rather than at each
copy-up, so descriptor delegation cannot reintroduce authorisation
against the acting task, and no reconfiguration can re-supply the
strata list.

There is no explicit per-stratum entitlement check in the mount path.
Both halves come out of the ordinary machinery: traverse rights are
enforced by the path walk, which runs under the mounting caller's
credentials, and the right to read a stratum directory's attributes is
enforced by using the security-checking `vfs_getattr` rather than its
unchecked variant, which reaches KACS's `getattr` hook and demands
`FILE_READ_ATTRIBUTES`.

One seam is worth recording: the path walk uses the credentials
captured on the filesystem context, while `vfs_getattr` runs outside
that override and so uses the acting task's. For a mount established
the ordinary way the two are the same task. For one established with
`fsopen` and `fsconfig` from different tasks they need not be.

Nothing re-checks entitlement if a stratum directory later appears.
That is sound because every access through the mount is checked against
the providing object in any case (§4.6.1), so a caller who mounts a
stratum they cannot read still cannot read it.

## 4.2.3.2 Validity

| Condition | Error |
|---|---|
| The stratum stack is empty | `EINVAL` |
| The stack exceeds 16 strata | `EINVAL` |
| More than one stratum carries `create` | `EINVAL` |
| A stratum carries both `create` and `ro` | `EINVAL` |
| The same directory appears as more than one stratum | `EINVAL` |
| A malformed `strata=` value (§4.2.2) | `EINVAL` |
| A create-bearing stack from outside the initial user namespace, or without `CAP_SYS_ADMIN` there | `EPERM` |
| A stratum's path names something other than a directory | `ENOTDIR` |
| A stratum's directory is absent and the stratum does not carry `am` | `ENOENT` |
| The composed stack reaches the kernel's maximum stacking depth | `ELOOP` |
| A stratum lies within the mount point, or within another stratafs mount whose strata include this mount point | `ELOOP` |
| Sixteen consecutive collisions allocating a mount cookie | `EAGAIN` |

Two strata are the same directory when they resolve to the same
directory object, not merely when their paths are equal as strings: the
comparison is on resolved inodes, so two paths reaching one directory
through different symbolic links or bind mounts are caught. Absent
strata are skipped and never compared.

## 4.2.3.3 Evaluation order

The conditions that depend on nothing but the option string are decided
first, during parsing and stack-wide validation, and are reported
whatever the caller's access. The `EPERM` admission test comes next, in
`get_tree`, still before any path is touched. Only then are the strata
resolved.

The purpose of that ordering is to stop the validity conditions being
an oracle: a caller with no right to traverse a directory should not be
able to name it as a stratum and learn from the errno whether it exists
and whether it is a directory.

For a single stratum, that holds. The path walk runs under the caller's
credentials, so a path the caller cannot resolve returns `EACCES` from
the walk itself, before the type test or the duplicate test is reached,
and the `EACCES` is propagated unchanged.

Across the stack it does not. The strata are checked in one loop —
resolve, stat, type-test, compare against earlier strata — so stratum 0
is fully judged before stratum 1 is resolved at all. A caller who names
a readable stratum first and an unreadable one second learns the first
stratum's `ENOTDIR` or `ENOENT` rather than the `EACCES` they would
have been given had the whole stack been checked for entitlement first.
This is tracked as a defect; the disclosure is bounded to paths the
caller could resolve, but the specified ordering is stack-wide.

The mount-point loop condition is evaluated in a different call
entirely, after the tree has been built, and so always follows every
entitlement check.

## 4.2.3.4 Loops

A stratum inside the mount point is detected directly. The indirect
case — a stratum inside *another* stratafs mount whose own strata
include this mount point — is detected by recursing into any stratum
whose superblock carries the stratafs magic, bounded by a visited-
superblock set and by the kernel's maximum stacking depth. This runs
through a Peios-added `super_operations` hook, `validate_mountpoint`,
wired into the new-mount, bind and move-mount paths by the patch series;
it is not an upstream interface.

That check cannot bind after the fact: a mount established within a
stratum, or a bind mount of this mount into one of its own strata, can
create a cycle later. Resolution is guarded separately, and differently
— not by a depth counter but by a per-task, per-superblock re-entrancy
list. A task that is already resolving in a superblock and re-enters it
gets `ELOOP` immediately, so a cycle spanning any number of stratafs
mounts terminates the moment it returns to one it is already inside.

## 4.2.3.5 Immutability and mount identity

The stack is fixed for the life of the mount; changing one is expressed
by unmounting and mounting again (§4.2.2).

Two stratafs mounts may name the same directory as a stratum. Each
resolves independently and neither is aware of the other; where both
have a create stratum in common they mutate the same objects, with the
same result as any two writers of one directory. There is no registry
of stratum paths to make them aware of each other.

There is a registry of *mounts*, but it exists for a different purpose.
Each mount draws a random non-zero cookie and inserts itself into a
global table, retrying up to sixteen times on collision; a second
random non-zero cookie is drawn once per boot. The pair identifies
which live mount owns a staging entry, so that a mount sharing a create
stratum can distinguish another mount's copy-up in flight from an
orphan left by a crash (§4.5.2).

A mount succeeds even when no stratum root is present at all — a stack
whose only strata are absent `am` strata is legal. The root inode is
constructed with no provider, and reports mode `S_IFDIR` with no
permission bits.

---

# 4.2.4 Absent Strata

_Peios / Advanced Peios / PKM / stratafs / Strata and Mounting_

> A stratum's directory may not exist — what happens while it is absent, and what appearing, disappearing and reappearing do.

A stratum's directory may not exist. It may be absent when the mount is
created — which requires `am` (§4.2.3) — or exist at mount time and be
removed while the mount is live, which nothing prevents for any
stratum.

## 4.2.4.1 While absent

An absent stratum holds no names, participates in no merged directory,
and contributes no entries to any enumeration. It keeps its position
and its precedence; only its presence bit is clear.

The mechanism is a single test in the resolver. Resolving one stratum
either succeeds, or fails with `ENOENT` or `ENOTDIR`, in which case the
stratum is skipped and its bit is left clear. Any other error —
`EACCES`, `EIO`, `ELOOP`, `ENAMETOOLONG`, `ESTALE` — is not treated as
absence and fails the whole resolution instead. So a stratum that is
unreadable for a reason other than not being there masks the name for
every stratum, rather than being passed over.

An absent create stratum additionally causes every operation that would
create or copy up to fail with `EROFS`. The create stratum's root is
re-resolved at the point of use and its `ENOENT` is mapped to `EROFS`
by each of the paths that needs it — the copy-up parent walk, the
creation authorisation pre-check, and the routing decision, which
computes create-stratum presence live and requires the result to be a
directory. stratafs does not create the stratum's own directory to
satisfy such an operation: establishing that directory, with the
security descriptor it should have, belongs to whatever provisions the
system, and a mount that minted it would be choosing that descriptor.

## 4.2.4.2 Appearing and disappearing

Neither event is detected. Both are simply observed, because there is
nothing to invalidate.

A stratum's directory is never held; only its path string is. Every
resolution walks that string afresh, so a directory that comes into
existence is picked up by the very next lookup, and one that is
removed, renamed away, or replaced by another directory is picked up
just as immediately — the walk finds nothing, or finds the replacement.

The specification describes this in terms of a version tuple recorded
over the nearest existing ancestor of an absent stratum's path, and an
identity comparison for a stratum that disappears. Neither exists in
the implementation, and neither is needed: they are the machinery for
knowing when a *cached* resolution has gone stale, and nothing is
cached. §4.4.2 covers the trade that represents.

The `am` flag is not consulted at runtime at all. It governs whether
the mount is allowed to be established with the directory missing, and
nothing more; a stratum without it that vanishes afterwards behaves
exactly like one with it.

## 4.2.4.3 Reappearance

A stratum that disappears and reappears is the same stratum in the same
stack position, and nothing about the old directory is remembered.
Resolutions are recomputed against whatever the new directory holds.

One piece of state does survive the gap, though it is not resolution
memory. The inode-number identity map (§4.4.3) is keyed on the provider
inode object and pins it for the life of the mount, so if the same
underlying inode is reached again — because the directory was renamed
away and back rather than replaced — it receives the same inode number
it had before. That is exactly what the identity rule requires: equal
numbers for one provider object, whatever path reached it.

---

# 4.3.1 Lookup

_Peios / Advanced Peios / PKM / stratafs / Name Resolution_

> Resolving one name in one directory — the provider, ancestors, independence from the caller, and staging entries.

Resolution is defined for one name in one directory. Every path
operation is a sequence of such resolutions, each independent of the
last.

## 4.3.1.1 The provider

To resolve a name in a stratafs directory, the corresponding directory
of each stratum is examined in precedence order, and the first stratum
holding an entry of that name is its **provider**. If no stratum holds
it, the resolution produces a negative dentry and the VFS reports
`ENOENT`.

Mechanically, each stratum's path string is joined with the relative
path of the name and walked in full, once per stratum. A walk that
succeeds sets that stratum's bit in a presence bitmap; a walk that
fails with `ENOENT` or `ENOTDIR` leaves it clear and the stratum is
skipped. Selecting the provider is then the trailing-zero count of the
bitmap, computed in `stratafs-core`.

Any other error from a stratum's walk — `EACCES`, `EIO`, `ELOOP`,
`ENAMETOOLONG`, `ESTALE` — is not treated as absence. It fails the
whole resolution, so a stratum that is unreadable for a reason other
than not being there masks the name entirely rather than being passed
over.

A joined stratum path, or a child relative path, that would exceed
`PATH_MAX` fails with `ENAMETOOLONG`.

## 4.3.1.2 Ancestors

Resolution does not consult a parent's provider to find a child's. A
name's provider is chosen afresh across all strata, so if `/a` is
provided by stratum 2, `/a/b` may still be provided by stratum 1 —
provided stratum 1 also holds `/a` as a directory and the two therefore
merge (§4.3.2).

What resolution does consult is whether any ancestor of the path is
masked. Before resolving the final component, every proper prefix of
the relative path is resolved across all strata and its merged provider
computed; a prefix whose provider is not a directory aborts the whole
resolution with `ENOTDIR`. That is what makes masking total (§4.3.3),
and it is why a lookup costs one full walk per stratum for the name
itself plus one merged resolution per path component above it.

## 4.3.1.3 Independence from the caller

Resolution runs under the credentials captured at mount, against the
root captured at mount, and takes no operation argument. It does not
depend on the calling token, on what the caller is trying to do, or on
whether the operation will ultimately be permitted.

A name whose provider the caller may not access therefore resolves
normally and is then refused. It does not fall through to a lower
stratum — which would let a caller's rights change which file they
read, a considerably worse property than a denial.

## 4.3.1.4 Reaching the object

Once a name resolves to a non-directory provider, the object is the
provider's object and stratafs does not interpose on its contents. The
outer inode takes the provider's mode and, from it, the operations
tables for a regular file, symlink or special file; reads, writes,
mappings, splices, locks and `ioctl`s are forwarded to a backing file
opened on the provider.

Symbolic links are forwarded rather than followed. The outer inode's
`get_link` calls the provider inode's own and returns the raw target
verbatim; stratafs deliberately does not use `vfs_get_link`, which
would demand a read right the caller need not hold to traverse a link.
The VFS then interprets the target in the caller's own namespace, so an
absolute target resolves from the process's root and may re-enter this
mount, another stratafs mount, or none.

Where a mount is established at a path within a stratum, stratafs
follows it: the stratum walk is an ordinary `filename_lookup` with no
flag restricting it to one filesystem, and everything downstream
operates on the inner mount it returns.

## 4.3.1.5 Staging entries

One class of name is invisible to resolution. While a copy-up is in
flight, its staged object may exist under a name in the create stratum;
that name is dropped from resolution for the mount that owns it, so an
incomplete copy is never reachable through the merged view (§4.5.2).
The suppression applies only to the create stratum, and only within the
owning mount — a second stratafs mount sharing that directory, and any
direct reader of it, sees an ordinary entry.

Staged names begin with `.stratafs-stage-`, and a lookup of any name
with that prefix triggers a recovery scan of the create-stratum parent
before resolving. A resolution can therefore have the side effect of
removing orphaned staging entries from the create stratum.

## 4.3.1.6 Recursion

A task that is already resolving inside a superblock and re-enters the
same superblock fails immediately with `ELOOP`. The guard is a global
list of task-and-superblock pairs, not a depth counter, so a cycle
formed after mount — a stratafs mount established inside one of its own
strata, or bind-mounted into one — terminates the moment resolution
returns to a mount it is already inside.

---

# 4.3.2 Directory Merge

_Peios / Advanced Peios / PKM / stratafs / Name Resolution_

> When lower strata hold the same name as a directory the result is a merged directory — participation, its create stratum, and symlinks.

Where a name's provider is a directory, and lower-precedence strata
hold the same name as a directory, the resolved object is a **merged
directory**.

## 4.3.2.1 Participation

The strata participating in a merged directory are, in precedence
order, those whose corresponding directory both exists and is a
directory. A stratum holding the name as a non-directory does not
participate and is masked entirely (§4.3.3); a stratum not holding the
name simply does not participate.

Every consumer of a merged directory applies the same two-part filter —
the presence bit is set, and the resolved dentry is a directory — in a
single ascending loop over stratum indices with a `continue` for
non-participants. Nothing compacts or re-sorts, so relative precedence
within a merged directory is always the relative precedence in the
stack.

Merging is recursive, and it is recomputed rather than cached: there is
no merged-directory object anywhere. Each lookup and each directory
open rebuilds the full per-stratum path set from the relative path
string, so any child that is a directory in more than one stratum
merges at its own level by the same rule.

The root of a mount is a merged directory whose participants are the
mount's strata. The root has one special case: where the ordinary
provider rule would pick a stratum root that is present but not a
directory, the root instead takes the first stratum that is both
present and a directory, so an absent or non-directory stratum root
cannot change the synthetic root's type. A mount whose stratum roots
are all absent still has a root inode, with no provider and no
permission bits.

## 4.3.2.2 The create stratum of a merged directory

A merged directory's create stratum is the correspondingly-named
subdirectory of the mount's create stratum, at the same path relative
to the mount root — whether or not that subdirectory currently exists.
It follows that a merged directory has a create stratum even when the
mount's create stratum holds no part of that path, and creation still
routes there.

The derivation is positional and mount-wide: `create_index` is a single
integer on the superblock, fixed at mount, and every creation and
copy-up site reads it directly. Nothing re-derives it from which strata
happen to participate.

That matters because the alternative — taking the create stratum to be
the highest-precedence participating writable directory — would make
the destination of a write depend on which directories happened to
exist, so creating a file in a subdirectory could land in a different
stratum from creating one beside it.

Where the create stratum's counterpart of a merged directory does not
exist, the path is materialised on demand, from the mount root
downwards, at the point an operation first needs it (§4.5.2). The
authorisation for creating into it is evaluated against the descriptor
that directory will carry once materialised, which is the corresponding
provider directory's (§4.6.2).

## 4.3.2.3 Symbolic links and participation

Two resolution entry points disagree about the final component of a
stratum path, and the difference is visible.

Ordinary lookup resolves without following the final component, so a
symbolic link at a name resolves to the link itself and the VFS follows
it. Building the participant set for a merged directory follows the
final component, and so do the emptiness scan, the foreign-entry scan,
the permission check and directory `fsync`.

The consequence is that a stratum holding a name as a symlink to a
directory **participates** in the merged directory, contributing the
target's entries to the merged listing and to emptiness tests, while a
stratum holding a dangling symlink at that name participates in
resolution but not in the participant set. Whether that is intended is
an open question against the specification, which describes a stratum
holding a non-directory as masked; it is tracked as a defect.

---

# 4.3.3 Type Conflicts

_Peios / Advanced Peios / PKM / stratafs / Name Resolution_

> Two strata holding one name with different types — the provider's type wins, masking is total, and the result is stable.

Two strata may hold the same name with different types — a directory in
one, a regular file in another. The provider's type is the type of the
resolved object, and the outer inode is given the provider's mode and
the operations tables that follow from it.

- Where the provider is a **directory**, lower-precedence strata that
  hold the name as a directory participate in a merged directory
  (§4.3.2). Lower-precedence strata that hold the name as anything else
  are masked.
- Where the provider is **not** a directory, every lower-precedence
  entry of that name is masked, whatever its type. Only the provider's
  path is retained on the dentry; the other strata's references are
  released as soon as the provider is chosen.

## 4.3.3.1 Masking is total

A masked entry is unreachable through the mount. Where the masked entry
is a directory, its entire subtree is unreachable: no path beneath the
masked name resolves, regardless of what the masked directory contains
and regardless of whether some other stratum holds part of that
subtree.

This is enforced by the ancestor pass described in §4.3.1. Each proper
prefix of a relative path is resolved across every stratum and its
merged provider computed; a prefix whose merged provider is not a
directory returns `ENOTDIR` before the final component is considered.
Because the test uses the merged answer rather than a per-stratum one,
another stratum holding the subtree cannot rescue it.

A regular file at `/x` in a high-precedence stratum therefore hides a
whole `/x/…` tree in a lower one. That is severe, and deliberate: the
alternative — resolving `/x` as a file but `/x/y` through the masked
directory — would make a path's meaning depend on how far along it the
caller looked.

Masking modifies nothing. Resolution and enumeration are read-only
throughout, no marker is written to any stratum, and the masked entry
remains present and unchanged in its own stratum, reachable by any path
that does not traverse the mount.

## 4.3.3.2 Stability

Type conflicts resolve identically for every caller and every
operation, because provider selection is a pure function of the
presence bitmap and consults neither.

An operation that would only be valid against the masked type does not
cause the masked entry to be selected. It fails against the provider
instead, with whatever error that type produces — typically `ENOTDIR`
from the ancestor pass, or `EISDIR` raised by the generic VFS against
an outer inode carrying the provider's mode.

---

# 4.3.4 Enumeration

_Peios / Advanced Peios / PKM / stratafs / Name Resolution_

> The union of names across participating strata, each appearing once — captured at open, with consistency and position rules.

Enumerating a merged directory yields the union of the names held by
its participating strata, with each distinct name appearing exactly
once.

## 4.3.4.1 Capture at open

The whole listing is built when the directory is opened, and the
enumeration is served from that capture for the life of the descriptor.
Each participant is opened and iterated in ascending stratum order, and
its entries are appended to one list.

Deduplication is global rather than per-stratum: before an entry is
recorded, the whole accumulated list is scanned for the same name, and
a match causes the entry to be dropped. Because participants are
visited highest-precedence first, the entry that survives is always the
provider's. `.` and `..` are dropped from every participant and
synthesised once.

Each surviving entry carries the name, its length, the directory entry
type reported by the providing participant, and an inode number
obtained by looking the child up in that same participant and mapping
the result through the identity table (§4.4.3), so `getdents` and
`stat` agree. The final component is not followed during that lookup,
so a symlink entry reports its own identity rather than its target's.

Two details of that: an entry that vanishes between the participant's
own `readdir` and the follow-up lookup is silently dropped, which is
ordinary provider behaviour; and a participant filesystem that reports
`DT_UNKNOWN` has that propagated unchanged, even though the child path
is in hand and the real type could be derived.

Shadowed entries are neither reported nor otherwise detectable. No
second record is ever allocated, so nothing about them survives into
the listing, and the entry count reflects distinct names only.

The order in which names are reported is stratum-ascending and then
each stratum's own `readdir` order — deterministic for one capture, and
not otherwise specified.

## 4.3.4.2 Consistency

There is exactly one capture per descriptor. Nothing appends to the
list after the directory is opened, and nothing re-captures — a
rewind replays the original capture, since the directory uses the
generic `llseek`. A change to any participating stratum after the open
is therefore invisible to that descriptor for its lifetime.

What that bounds is only what stratafs itself contributes. Each
participating directory is enumerated by its own filesystem, with
whatever consistency that filesystem offers its own callers, and
participants are read sequentially with no cross-stratum lock or
barrier. A merged listing is no better than the listings it is
assembled from, and nothing claims otherwise.

## 4.3.4.3 The settled participant set

For the purpose of enumeration, the participating set is settled when
the directory is opened and does not change for the life of the
descriptor. What is settled is the set of participating **directory
objects**, not the set of stratum positions: the descriptor holds a
`struct path` reference on each participant, pinned until release, and
nothing re-resolves those positions by path. A participant that has
since been removed and replaced by another directory at the same path
is a different object and contributes nothing.

The settled set has exactly three consumers: the enumeration itself,
the access check performed when the directory is opened (§4.6.2), and
the origin attribute read through that descriptor (§4.7). It extends to
nothing else. Resolving a name relative to the descriptor — opening,
removing, renaming, linking — is an ordinary live resolution under
§4.3.1, performed against the strata as they are at that moment.

So a descriptor opened before a stratum began to hold the directory
will not list that stratum's names, but `openat` through the same
descriptor will resolve them. The two answers differ deliberately:
enumeration is a bulk disclosure whose authorisation was decided once,
when the descriptor was opened; a resolution is a fresh operation that
carries its own check.

Freezing the participant set is what keeps that open-time check
meaningful. Were a later re-read free to admit a stratum that joined
afterwards, its names would reach the caller without its directory's
descriptor ever having been consulted — and a stratum owner could make
a directory participate precisely to expose it. A caller that wants the
current participant set reopens.

## 4.3.4.4 Positions

Offsets 0 and 1 are `.` and `..`. Offset `2 + k` is the k-th element of
the captured list — a plain ordinal. The offset each participant
filesystem supplies is discarded: no provider cookie, no stratum index,
and no name hash is encoded.

A position is therefore meaningful only within one open file
description. On close and reopen — and so across a remount or a reboot —
the capture is rebuilt from each stratum's current contents and current
`readdir` order, and ordinal `2 + k` may name a different entry or
none. `telldir` and `seekdir` across descriptors are unreliable on a
stratafs directory, as is NFS re-export of one.

## 4.3.4.5 Access

Enumeration requires traverse and list rights on **every** participating
directory, checked before the capture is built. The check returns on
the first refusal, and a refusal aborts the open entirely, so no
partial listing covering only the readable strata can be produced.

---

# 4.4.1 The Coherency Model

_Peios / Advanced Peios / PKM / stratafs / Coherency_

> Strata change underneath stratafs without telling it — what can change a resolution, and the guarantee that survives it.

Every stratum of a mount may be modified at any time by an agent that
does not know stratafs exists. stratafs observes such changes without
being told of them.

## 4.4.1.1 No coordination

There is no notification machinery anywhere in the filesystem — no
`fsnotify` registration, no `inotify`, nothing a stratum's filesystem
is expected to report. No writer announces a change, quiesces, or
participates in any protocol. Every resolution is performed from
scratch by re-walking the stratum path string, so nothing has to be
told anything.

## 4.4.1.2 What can change a resolution

A resolution depends only on which names each participating stratum
directory holds and what type each entry has. It does not depend on the
contents of any file.

A change to an object's **contents** therefore requires no action at
all. stratafs inodes carry no address-space operations; there is no
second page cache, and every data operation is forwarded to a backing
file opened on the provider. A change to that object is observed
through the mount immediately and by construction, because there is
nothing to invalidate.

A change to the **structure** of a participating stratum directory — an
entry created, removed, renamed, or replaced by one of another type —
may change which stratum provides a name. §4.4.2 covers how that is
handled, which is by not caching anything.

## 4.4.1.3 The guarantee

A structural change in a stratum becomes visible to any resolution
begun after the stratum's own filesystem exposes that change to an
ordinary lookup. stratafs adds no delay of its own: there is no
timeout, no jiffies comparison, no generation counter, and no
resolution cache to serve a stale answer from.

It cannot anticipate a change the underlying filesystem is not yet
reporting. A network filesystem holding an attribute cache does not
show a change to stratafs any sooner than to any other caller, and
nothing claims otherwise.

Resolutions already completed are not revisited. Every regular-file
open is detached onto a descriptor-private dentry and inode holding
their own reference to the provider, and I/O runs against the file
opened at that time, so later masking or removal in any stratum cannot
reach that descriptor. It is not re-pointed and it does not fail. A
process holding a configuration file open across a package upgrade
continues to read the file it opened.

The one exception is copy-up, which §4.4.3 covers: a descriptor whose
own write caused a copy-up keeps its inode while that inode's backing
object becomes the copy.

## 4.4.1.4 Live strata

Because resolutions are made against current state, and because a
stratum is a path rather than a directory object (§4.2.1), a stratum's
directory may be replaced wholesale — by a package transaction, by a
reconciler, by an administrator — while the mount is live, with no
remount and no interruption to callers. That is the requirement the
filesystem exists to satisfy, and §4.4.2 is the whole of its cost.

---

# 4.4.2 Revalidation

_Peios / Advanced Peios / PKM / stratafs / Coherency_

> The caching the specification permits, the version tuple and identity comparison it rests on, and what always invalidating costs.

The specification permits an implementation to cache resolutions,
subject to a version tuple recorded per stratum and an identity
comparison on reuse. This implementation caches nothing, and so
implements neither.

## 4.4.2.1 Always invalidate

`d_revalidate` returns 0 for every dentry except the mount root. The
VFS therefore discards the dentry and re-enters `lookup` on every path
walk, and the lookup re-resolves the parent and the child from their
relative path strings across every stratum.

Nothing is memoised. No version value is recorded, no directory
identity is retained for comparison, no nearest-existing-ancestor walk
happens, and no `i_version` is read anywhere in the filesystem. The
per-dentry provider path that is retained is not a cache of a
resolution: it is dropped when the dentry is released, and the dentry
is released on the next walk.

The specification's machinery exists to know when a cached resolution
has gone stale. With no cached resolution, the questions it answers do
not arise:

| What the tuple would detect | Why it is unnecessary here |
|---|---|
| A stratum gaining or losing a name | The next walk resolves the name afresh |
| A stratum's directory appearing | The next walk finds it |
| A stratum's directory removed, renamed away, or renamed over | The next walk finds nothing, or finds the replacement |

The result is strictly stronger than the specification requires, in the
safe direction. The two internal counters the superblock does carry —
the inode-number allocator and the staging-name counter — are consulted
by nothing in this path.

## 4.4.2.2 What it costs

The cost is real and lands on the hottest path in the kernel.

`d_revalidate` refuses RCU-walk unconditionally: a lookup in RCU mode
returns `ECHILD` and the walk is retried in ref-walk mode, and the
directory permission check does the same. RCU-walk is therefore never
used on a stratafs mount, and the fallback is taken always rather than
only where it genuinely cannot proceed.

What replaces it, per path component, is one full `filename_lookup` per
stratum — up to sixteen — plus one merged resolution per proper prefix
of the path, for the ancestor-masking test of §4.3.3. Where a mount is
established over a directory of executables, that lies on the path of
every program execution, and there is no dentry cache hit to avoid it.

This is the one part of the filesystem whose cost is worth measuring
rather than assuming, and closing the gap — recording enough per
resolution to reuse one safely, and making the comparison RCU-safe — is
tracked as work in its own right.

---

# 4.4.3 Inode Identity and Lifecycle

_Peios / Advanced Peios / PKM / stratafs / Coherency_

> stratafs allocates its own inodes standing for provider objects — the identity it reports, the mount root, and what a provider change does.

stratafs presents its own inodes, allocated from its own superblock.
Each stands for a provider object and forwards operations to it; none
holds file data, directory entries, or a security descriptor.

## 4.4.3.1 Reported identity

The device identifier reported for any object in a mount is the
stratafs superblock's own anonymous device, not the provider's.
`getattr` calls the provider's directly and then overrides `dev`, `ino`
and, for directories, `nlink`; all four inode-operations tables install
that same `getattr`, so there is no path around it.

The inode number is **allocated, not derived**. A per-mount monotone
counter hands out a number for each distinct provider inode the first
time it is seen, and the pair is recorded in an `xarray` on the
superblock keyed on the provider inode, holding a reference on it so
the object cannot be freed and its address reused while the mount
lives. The counter starts at 1 and is pre-incremented, so the first
number handed out is 2. The provider's own inode number and device are
never read for this purpose.

That satisfies the identity requirements — two names compare equal
exactly when they resolve to one provider object, since the map is
keyed on the object itself and not on the stratum that reached it, so
hard links compare equal and one directory reached through two strata
compares equal too. It does not follow the specification's advice to
derive the number from the provider's, and it therefore pays the cost
that advice exists to avoid: the map is never evicted from, and it pins
every provider object ever reached through the mount until unmount. A
mount whose strata would not have provoked the fallback pays it anyway.

Two mechanical details. The map key is the provider inode's kernel
address shifted right by three; a collision would be caught by the
stored back-pointer and turned into an allocation failure rather than a
false equality, so it fails rather than lying. And the stored number
and `i_ino` are `unsigned long` while the counter is 64-bit, so on a
32-bit build the number truncates.

### 4.4.3.1.1 The mount root

The root inode is created once, when the superblock is filled, and its
number is never recomputed — `d_revalidate` returns 1 for the root, so
it is never replaced. Its provider *is* re-resolved live on every use,
so the root's mode, owner and timestamps are always current; only the
number is not.

When the root's provider changes — a higher-precedence stratum root
appears, or the mount-time one is removed — `stat` on the mount point
keeps reporting the number allocated for the mount-time provider. Where
no stratum root existed at mount, it reports a bare counter value
corresponding to no provider object at all. If the root's current
provider is also reachable at some other merged path, that path reports
the object's mapped number while the root reports its stale one, so two
paths naming one object disagree. This is tracked as a defect.

## 4.4.3.2 Attributes of merged directories

A merged directory's owner, group, mode and timestamps are its
provider's — the highest-precedence participating directory — taken
straight from a `getattr` on the provider path, so what is reported is
a real directory's attributes rather than a composite.

Its link count is forced to 1, both in the cached inode and in the
reported `stat`. The true count of subdirectories spans strata and
cannot be maintained, so the value carries no meaning beyond indicating
that the object is a directory. Nothing should infer a subdirectory
count from it.

The security descriptor of a merged directory is not a single
descriptor; §4.6.2 defines which participating directory's descriptor
governs each operation.

## 4.4.3.3 Provider change

When the provider for a path changes — because a higher-precedence
stratum gained the name, because the previous provider's entry was
removed, or because copy-up produced a new object — a **resolution** of
that path yields a new inode. An inode reached by resolution is never
re-associated with a different provider.

That is required because per-inode state is populated from the provider
the inode was resolved against and is not in general re-derivable. In
particular KACS caches a security descriptor against the outer stratafs
inode; applying it to a different provider's object would govern access
to one object by another object's descriptor.

The implementation enforces this bluntly. The one function that
re-associates an inode with a new provider is reachable from exactly
one call site, on the descriptor-private dentry created at open, and
never on a hashed dentry reached by resolution. A copy-up performed for
a path rather than a descriptor drops the dentry instead of rebinding
it. Provider change by masking or removal needs no special handling at
all, because the unconditional invalidation of §4.4.2 forces a fresh
lookup and a fresh inode.

An object's reported inode number therefore changes when its provider
changes. That is expected: it is the same change a caller would observe
if the file had been replaced, which is what has happened.

### 4.4.3.3.1 Descriptors already open

A descriptor open at the moment its own operation copies its object up
is not re-pointed. It keeps the inode it was opened against, and that
inode's backing object becomes the copy: the provider path and provider
inode are swapped in place under a per-inode lock, the attributes are
refreshed, and the backing file is replaced, so subsequent operations
through the descriptor reach the copy.

This is the one case in which an inode's backing object changes, and it
is safe for the reason the general rule exists: copy-up preserves the
source's security descriptor exactly (§4.6.3), so the descriptor cached
on that inode remains correct for the copy.

At open, the descriptor-private inode is deliberately given the path
inode's number, so before any copy-up `fstat` and `stat` agree. After
one they do not: the descriptor keeps the number allocated for the
pre-copy-up provider, while a fresh resolution of the path allocates a
number for the copy. Both name the copy; the numbers disagree for as
long as that descriptor lives. §4.8 records it.

## 4.4.3.4 Lifetime

A stratafs inode holds a reference on its provider inode for as long as
it lives, released on eviction. The dentry additionally holds a full
path reference, and the identity map a third, held until unmount.

Releasing the last reference to a stratafs inode modifies nothing:
eviction truncates its own empty mapping, clears the inode, drops the
provider reference, and frees its private state.

---

# 4.5.1 Write Routing

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> Which single stratum a modification is performed against, when that decision is made, and what shared writable mappings do to it.

An operation that modifies an existing object is performed against
exactly one stratum. This section covers which, and when the decision
is made.

## 4.5.1.1 Accepting modification

A stratum **accepts modification** of an object it provides when all
three of the following hold:

- the stratum does not carry `ro`;
- the provider's mount is not read-only;
- the provider's inode is not marked immutable.

The predicate is a property of the stratum and the object alone. It
takes the superblock, the stratum index and the provider path, and
nothing else — no credentials, no security descriptor, no access
check.

That restriction is load-bearing. Were the predicate to take the
caller's rights into account, a caller *refused* write access by the
provider's descriptor could still provoke a copy-up: the write would
fail, but the copy would have been published, and from then on the
merged path would resolve to a snapshot the provider's legitimate
writer could no longer update. A caller with no write access at all
could freeze any file in the mount.

Note the third term is the immutable inode flag specifically, not
unwritability in general. A file that is unwritable by its mode bits is
routed in place and refused by the underlying filesystem.

## 4.5.1.2 The rule

The decision itself is `route_existing` in `stratafs-core`, which takes
the provider index, whether it accepts modification, the create index
and whether the create stratum is present, whether the object is of a
copyable type, and whether the stratafs mount itself is read-only. It
returns one of three routes.

1. If the **mount** is read-only, the route is read-only. This term
   short-circuits everything else.
2. If the provider accepts modification, the operation is performed
   against the provider's object.
3. Otherwise, if the object is copyable, a create stratum exists, is
   present, and has **strictly higher precedence than the provider**,
   the object is copied up and the operation performed against the
   copy.
4. Otherwise the route is read-only and the operation fails with
   `EROFS`.

The strict `create_index < provider` comparison is what stops a
modification being placed where something already present would shadow
it. Where a high-precedence stratum provides a name it will not accept
a write for, there is no lower stratum that can take the write without
the result vanishing behind the provider, so `EROFS` is the honest
answer — reported at the moment of the write rather than discovered
later.

Where the name is held by no stratum, the operation is a creation and
§4.5.3 applies instead.

## 4.5.1.3 When the decision is made

Routing happens when a modifying operation is performed, not when a
descriptor is opened. Every mutating entry point calls
`route_existing` afresh against the provider it currently holds;
nothing caches a route decision, and a descriptor opened before the
predicate changed is not revisited.

| Operation | Routes |
|---|---|
| Writing or appending | Yes |
| Truncating, or any other `setattr` | Yes |
| Changing mode, owner, timestamps, or the security descriptor | Yes |
| Setting or removing an extended attribute | Yes |
| `fallocate` | Yes |
| `splice` into the file | Yes |
| `copy_file_range` and `remap_file_range` | Yes |
| Establishing a shared writable mapping | Yes |
| Reading contents, attributes, or extended attributes | No |
| Opening, for any access | No |
| Taking or releasing a lock, or a lease | No |

The security descriptor and the system access control list are both
reached as extended attributes, so both route through the `setxattr`
path like any other attribute; there is no descriptor-specific code in
stratafs at all.

An open is not itself a routing trigger. `open` computes a route, but
uses it only to decide the flags of the backing open — a non-in-place
route downgrades the provider open to read-only and strips `O_TRUNC`,
deferring rather than deciding. The one case in which an open acts is
`O_TRUNC` on a regular file: that is a modification, so it routes, and
copies up or fails with `EROFS` there and then. The truncation itself
is applied afterwards by the VFS through `setattr`, which routes again
and lands on the copy.

Routing at operation time rather than at open is what makes the rule
implementable. A filesystem cannot see what an open asked for in
access-mask terms — the caller's descriptor is stamped by KACS before
the filesystem's own open method runs, and its mask lives in a private
blob a filesystem cannot reach. Nor does it need to: the access check
has already happened by the time an operation reaches stratafs, so a
modifying operation arriving here is one its caller was entitled to
perform, and routing it is a decision about strata alone.

## 4.5.1.4 Shared writable mappings

Establishing a shared writable mapping routes, even though no bytes
have been written, because stores through such a mapping reach the
object without any further filesystem operation — establishment is the
last point at which routing can occur.

The test is on `VM_SHARED` together with `VM_MAYWRITE`, the "could
become writable" bit rather than the "is writable" bit. A `PROT_READ`
shared mapping taken from a writable descriptor therefore routes,
because it can acquire write access later through `mprotect` with no
filesystem operation in between. A private mapping, and a shared
mapping that cannot acquire write access, do not route. Where routing
yields read-only, the mapping is refused with `EROFS`.

## 4.5.1.5 What a copy-up does to an open descriptor

A copy-up performed for one descriptor changes which object provides
the name. That descriptor refers to the copy from then on: it keeps the
inode it was opened against while the inode's backing object becomes
the copy (§4.4.3), and its backing file is replaced.

Every other descriptor already open against the original continues to
refer to the original, and any fresh resolution of the path yields a
new inode standing for the copy. The pre-copy-up file is not closed —
it is retained so that locks and leases taken before the copy-up keep
working (§4.5.7).

## 4.5.1.6 Special files

A FIFO, socket, or device node is opened and written without the
filesystem object being modified: what is written passes to a pipe, a
socket, or a driver, not to the object's contents. Writing to such an
object therefore does not route, and such an object is never copied up
— every routing site gates on the object being a regular file, and the
copyable flag excludes anything that is not a regular file, directory
or symlink.

Copying up a FIFO would sever it: a reader holding the original and a
writer that arrived after the copy would hold two unrelated pipes. A
device node survives copying only by the accident that the copy names
the same device.

Opens, reads and writes are forwarded to the provider whether or not it
accepts modification, since the write-mode downgrade and the `O_TRUNC`
refusal both apply to regular files only. Operations that modify the
object itself — its mode, its descriptor, its extended attributes — do
route, and where the provider does not accept modification they fail
with `EROFS`, because the copy-up branch cannot apply to an object that
is never copied up.

## 4.5.1.7 `ioctl`

`ioctl` on a regular file is refused unconditionally with `ENOTTY`, and
its compat form with `ENOIOCTLCMD`. Stored-file `ioctl`s can mutate
data and would need command-by-command routing, which is not
implemented; refusing is the conservative stand-in. `ioctl` on a
non-regular file is forwarded to the provider.

---

# 4.5.2 Copy-Up

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> Replicating an object from its provider into the create stratum — parents, what is replicated, staging and atomicity.

Copy-up replicates an object from its provider stratum into the create
stratum, at the same path relative to the mount root, so that a
modification can be applied without modifying the provider.

Every step runs inside a KACS copy-up context, which exempts the
mechanics from caller authorisation without granting anything. §3.9.7
describes that context in full; §4.6.3 covers the stratafs side of the
bargain.

## 4.5.2.1 Parents

Where the create stratum does not hold the directories containing the
object's path, they are created first, walking the relative path
component by component from the create-stratum root downwards. Each is
created as an empty directory with the mode of the corresponding
**merged provider** directory, and with that directory's security
descriptor, installed by KACS during the create phase rather than
inherited.

Contents are not copied. The directories exist to hold the copied
object; the entries they hold in lower strata continue to be reached by
merging.

Where the create stratum holds one of those components as something
other than a directory, the copy-up fails with `ENOTDIR` and the
operation requiring it fails. The blocking entry is not removed or
replaced — there is no `unlink`, `rmdir` or `rename` anywhere on the
parent-materialisation path.

Materialised parents receive a mode and a descriptor, and nothing else:
no extended attributes and no timestamp preservation.

## 4.5.2.2 What is replicated

| Provider type | Result in the create stratum |
|---|---|
| Regular file | A regular file with the same contents and mode |
| Symbolic link | A symbolic link with the same target |
| Directory | An empty directory with the same mode; contents are not copied |

A device node, FIFO, or socket is never copied up. Anything that is not
a regular file, directory or symlink is refused with `EROFS` before a
copy-up begins.

The security descriptor is carried by KACS rather than replicated as an
attribute (§4.6.3). Every other extended attribute is copied, with
three exclusions: the canonical descriptor attribute, anything in the
`system.stratafs.` namespace, and the staging marker attribute. Each
is copied with `XATTR_CREATE`, and `security.capability` goes through a
dedicated KACS entry point rather than a raw write.

No attribute is silently discarded. Any per-attribute failure aborts
the copy-up, as does a listing that fails or exceeds `XATTR_LIST_MAX`;
in every case the error reported is `EIO`, whatever the underlying one
was.

Modification timestamps are preserved for regular files and for
directories, and **not** for symbolic links, which receive the current
time. That is a divergence from the specification's `SHOULD` and is
tracked as a defect. Access and change times are not preserved for any
type.

**Ownership is not preserved.** The staged object is created with the
calling task's credentials, and the metadata copy transfers only the
mode and the modification time — there is no uid or gid transfer
anywhere. The KACS security descriptor, including its owner SID, *is*
preserved, so the descriptor-level owner is the source's; the POSIX
owner is the caller's. Since disk quota keys on the POSIX owner, a copy
is accounted to the caller who caused it rather than to the owner of
the object it was copied from, which is the opposite of what §4.5.8
describes. This is tracked as a defect.

Hard links are not preserved: an object with several links in its
stratum is copied up as a single independent object, and the other
links continue to refer to the original.

Contents are copied through a 64 KiB buffer, one read and one write per
iteration, with the inner write loop retrying short writes. Where the
copy-up was provoked by a path rather than a descriptor, the source is
reopened for each chunk; the staged file is reopened for each chunk
either way.

## 4.5.2.3 The source must still be the provider

Before beginning a copy-up on behalf of a descriptor, the object the
descriptor refers to is verified still to be the provider of that name,
comparing both the path and the provider inode. Where it is not, the
copy-up is not performed and the operation fails with `ESTALE`.

The verification is repeated immediately before publication, under the
create directory's lock, and publication itself uses an operation that
fails if the target name already exists: linking an anonymous object
into place, or a `RENAME_NOREPLACE`. Both additionally test the target
dentry explicitly, and an `EEXIST` from either is normalised to
`ESTALE`.

That is decisive for the case that matters. A competing copy-up
publishes into the same create-stratum directory, where the
filesystem's own atomicity applies, so exactly one of two racing
copy-ups succeeds and the other fails `ESTALE`.

It cannot extend further, and nothing tries to. Strata may be on
different filesystems, and stratafs can neither lock them together nor
inspect them at one instant. A change in some *other* stratum after the
second verification — a direct writer creating the name in a
higher-precedence stratum, say — is an ordinary concurrent structural
change, visible to the next resolution, and is neither an error nor
detected.

This is not a rare case. Every descriptor other than the one that
caused a copy-up still refers to the original object, so a second
descriptor opened before the copy-up meets this rule the first time it
writes. A caller therefore has to be prepared for a write to fail
`ESTALE` on a descriptor that was valid when it was opened and has done
nothing wrong. §4.8 records it.

## 4.5.2.4 Staging and atomicity

A copy-up is never observable in a partial state. All content,
attribute and metadata work happens on an object that is not reachable
through the mount, and publication is a single step.

Two arrangements are used, chosen by type:

- A **regular file** is staged as an anonymous object on the create
  stratum's filesystem — a kernel tmpfile — and linked into place when
  complete. No reader can reach it.
- A **directory or symlink** is staged under a name in the create
  stratum, since neither has an anonymous form, and published by a
  no-replace rename.

A staged name is `.stratafs-stage-` followed by the mount cookie and a
per-stage identifier, in a 64-byte buffer, with up to eight retries on
collision. It is excluded from resolution and from enumeration for the
mount that owns it, and removed if the copy-up fails. The exclusion is
local: a second stratafs mount sharing the create stratum, and every
direct reader of that directory, sees an ordinary entry containing a
partial copy.

### 4.5.2.4.1 Identifying staging entries

A staged name alone is not proof of ownership, so each staged object
also carries a marker in the extended attribute
`security.peios.stratafs_staging`. The marker is a 24-byte
little-endian structure: a magic of `0x53544731` — ASCII `STG1` — a
version, its own size, a per-boot cookie, and the cookie of the mount
that created it.

Recovery of an orphan is therefore precise. A staged entry whose marker
is valid and whose owning mount is not live is removed; one belonging to
a live mount is left alone, and so is one whose marker is missing,
short, or carries the wrong magic, version or size. That matters
because two mounts may share a create stratum, and an unqualified
cleanup would have each new mount destroy the other's copy-up in
flight.

The scan runs in batches of 128 names, with resume, and is triggered
from five places: mounting, opening a directory, looking up a name
beginning with the staging prefix, copying up into a directory, and the
emptiness test of §4.5.4 where it finds a directory empty but saw
staging entries in it.

The fifth exists because the other four only reach directories somebody
visits. An orphan in a directory that is never opened, looked up, or
copied into would otherwise never be removed, and the emptiness test —
which filters staging entries — would report its parent empty and let
an `rmdir` proceed that then failed at the provider. Recovering on that
path costs nothing until someone actually meets the case, and reaches
directories that a recursive walk of the create stratum at mount would
have to visit every one of to find.

The marker is removed after publication. A failure to remove it is only
warned about, leaving the marker on the published copy until a later
lookup cleans it.

Where a copy-up fails for any reason, the create stratum is left with
no new entry at the target path and the operation fails. Nothing falls
back to a weaker replication: an object whose descriptor or extended
attributes could not be preserved is never published. A published copy
whose descriptor handoff then fails is rolled back.

## 4.5.2.5 Concurrent modification

The provider may be modified while it is being copied, by a writer that
does not know stratafs exists.

Copy-up reads the provider as any reader would, with no locking of the
source and no snapshot, and offers no stronger consistency than an
ordinary read of that object. Where the provider is modified during the
copy, the copy may contain bytes from more than one state of the
source, exactly as a concurrent `read` of the same object may.

Nothing detects that and restarts — there is no retry loop, no
generation check. Equally, nothing blocks waiting for a quiescent
source, and nothing fails a copy-up merely because the source is being
written.

What *is* guaranteed is that the published object never appears in a
partial state, because staging and publication are properties of the
create stratum's filesystem, which stratafs does control.

Once published, the copy is independent of its source. Subsequent
modifications to the object it was copied from are not reflected in it,
and are not visible through the mount for as long as the copy provides
the name.

---

# 4.5.3 Creation

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> Creating a name no stratum holds — where it lands, its security descriptor, deleting dispositions, and exclusive against non-exclusive creation.

Creating a name held by no participating stratum places it in the
create stratum.

1. If there is no create stratum, or it is absent, the operation fails
   with `EROFS`.
2. Any directories of the create stratum containing the name's path
   that do not exist are materialised as §4.5.2 requires for copy-up
   parents. Where the create stratum holds one of those components as
   something other than a directory, the operation fails with
   `ENOTDIR`; the blocking entry is not removed or replaced.
3. The name is created there.

All six kinds of creation — regular file, directory, symbolic link,
device node, FIFO, socket — go through one helper and one path.

The `ENOTDIR` case arises because creation routes positionally, into
the create stratum's subdirectory at the same path whether or not it
exists (§4.3.2). Where the create stratum holds that path as a file,
and a higher-precedence stratum provides it as a directory, the merged
directory exists and is reachable while its create-stratum counterpart
is blocked.

## 4.5.3.1 The security descriptor

A created object has no provider to inherit from, so its descriptor is
established by the ordinary creation semantics of the create stratum's
filesystem — by inheritance from the directory it is created in,
exactly as if it had been created there directly. The create is an
ordinary `vfs_create`, `vfs_mkdir`, `vfs_symlink` or `vfs_mknod`
against the real create-stratum parent, with the KACS creation decision
bound to that same parent.

This is cleanly separated from copy-up inside KACS: the copy-up branch
of the inode security initialisation is taken first and short-circuits
the inheritance builder entirely. Ordinary creation in a create stratum
inherits; copy-up preserves.

Where the creating interface lets the caller supply a descriptor — the
native open path does — it is honoured rather than replaced by an
inherited one. That is entirely KACS's doing; stratafs has no
descriptor parameter and cannot express it. All stratafs contributes is
re-anchoring the pending native create request onto the create-stratum
parent.

## 4.5.3.2 Dispositions that delete

A creating interface may offer a disposition that replaces an existing
object rather than opening it. In Peios that is the supersede
disposition of the native open path: KACS creates a temporary file
through the mount, opens it, and renames it onto the target, at which
point stratafs diverts the rename into a dedicated supersede path.

Such a disposition is a removal followed by a creation, and both halves
apply. Before anything is created, the removal is validated: the
target's provider must accept modification, or the operation fails with
`EROFS`. Before anything is removed, the replacement is checked for
reachability: if any stratum strictly above the create stratum, other
than the provider being removed, holds the name, the operation fails
with `EROFS`.

Without that guard the disposition could report success while leaving
the caller's new object invisible. The removal takes the name out of
the stratum that provided it; the creation puts the replacement in the
create stratum; and if some stratum between the two also holds the
name, it now outranks the replacement.

The creation half is treated as a creation throughout. It is never
reclassified as a modification of a newly-surfaced provider and never
copies one up: the caller asked for a new object, and its descriptor is
derived as above.

Three restrictions are implementation choices rather than consequences
of the model. Supersede applies to regular files only, and anything
else is refused with `EOPNOTSUPP`. The source must be in the create
stratum, and source and destination must share a parent. And the two
halves are **not** atomic: the lower entry is unlinked first and the
staged file renamed into place afterwards, so a failure in between
leaves the name having lost its old provider. The caller receives the
error; the window itself is recorded only in the audit trail.

## 4.5.3.3 Deferred deletion

A request to delete an object when the last descriptor to it is closed
applies to the object the descriptor resolved to, not to whatever
provides that name at close time. Because removal (§4.5.4) is defined
over the current provider of a name, the deferred case has its own
path.

When the deletion is attempted, stratafs locates the entry at the path
the descriptor was opened against, in the stratum that provided it at
that time — the descriptor's private dentry carries both — and resolves
the parent in that same stratum. Four conditions end the attempt
quietly, reporting success because the deletion is already complete:
the parent is gone, the name is gone, the name now identifies a
different inode, or the unlink raced.

That stratum must accept modification, or the deletion fails with
`EROFS`. Then the entry is removed, and no other. In particular, where
another caller's copy-up has published a new object at that name in a
higher stratum, that object is not the one being deleted and is left
alone.

Because the attempt has no caller to report to, any failure is audited
— and for a deferred deletion, *every* non-zero result is audited, not
only the arrangement errors §4.6.5 covers for ordinary refusals. The
object is left in place.

One part of the model is not implemented as specified. The right to
delete an entry is checked when the delete-on-close request is armed,
against the requesting token, which is correct. But it is checked
against the **merged** parent directory rather than against the
directory of the stratum where the entry actually lives, and no check
is made at deletion time. The specification requires the check to name
that stratum's directory specifically. This is tracked as a defect.

Deferred deletion is restricted to non-directories, and at arm time to
regular files on a managed mount.

## 4.5.3.4 Exclusive creation

Where creation is requested exclusively, the name must not exist in
**any** participating stratum, and a name provided by a lower stratum
causes `EEXIST` even though the create stratum does not hold it.

Nothing in stratafs implements this, and nothing needs to. The merged
lookup instantiates a positive dentry whenever any stratum holds the
name, and the VFS refuses `O_EXCL` on a dentry it did not create. The
`excl` argument stratafs receives is ignored. There is a race backstop
in the create path — a positive dentry appearing in the create stratum
yields `EEXIST` — but it sees only that one stratum.

Exclusive creation asks whether the name is free, and through this
mount it is not: a caller that created it anyway would find their
object shadowing a file they did not know was there, or masking a whole
subtree.

## 4.5.3.5 Non-exclusive creation over a shadowed name

Where creation is not exclusive and a lower stratum provides the name,
the merged dentry is positive, so the VFS never calls the create path
at all. The operation is an open, and §4.5.1 routes it.

Where the provider is a regular file that does not accept modification
and the create stratum has higher precedence, the result is a copy-up
followed by the requested modification, including truncation where
`O_TRUNC` was requested. `O_TRUNC` is stripped from the backing open on
a non-in-place route precisely so that the copy-up source is not
destroyed before it is read.

Where the provider is a FIFO, socket, or device node, no copy-up occurs
and the open is forwarded to the provider. Such an open succeeds
whether or not the provider accepts modification: opening a special
file does not modify it.

## 4.5.3.6 Unnamed files

A file may be created without a name, for later linking into place. In
a merged directory this is supported, and the file is created on the
create stratum's filesystem, recorded with the create stratum's index
and marked unnamed.

Where the mount has no create stratum, or it is absent, the operation
fails with `EROFS`. Parent materialisation applies as for a named
creation: the create stratum's counterpart of the directory the
operation named is materialised, and the new file's descriptor is
derived by inheritance from it, because that directory is what the
descriptor must come from. Linking such a file into the mount is
governed by §4.5.6.

---

# 4.5.4 Removal

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> stratafs has no whiteouts, so removal can only remove an entry that is really there — and what removal therefore never does.

stratafs has no whiteouts and cannot record that a name should be
absent. Removal can therefore only remove an entry that is actually
there, in a stratum it may write to.

## 4.5.4.1 Unlinking

To remove a non-directory name from a merged directory: if the provider
accepts modification (§4.5.1), the entry is removed from the provider;
otherwise the operation fails with `EROFS`. The parent is resolved in
the provider's stratum, and the unlink is performed there.

The provider need not be the create stratum. Any stratum that accepts
modification may have an entry removed from it, by the same rule that
allows an object it provides to be modified in place.

Where a lower-precedence stratum also holds the name, that entry
becomes the provider once the higher one is removed, and the name
remains visible, now resolving to the lower stratum's object. That is
not an error and is not reported as one: the removal succeeded, and the
entry it removed is gone. The dentry is dropped on success, so the next
lookup finds the lower entry.

Removing an object that a lower stratum also provides is how a
modification is undone. Where a file was copied up in order to be
edited, removing it through the mount discards the edit and restores
the original, which remained untouched in its own stratum throughout.
Callers expecting POSIX removal will find the name still present
afterwards; §4.8 records this as an intended divergence.

Refusal because the provider does not accept modification is `EROFS`,
with one exception: where the provider's inode is immutable it is
`EPERM`. The outer inode carries the provider's inode flags, so the VFS
refuses the removal before stratafs's own `EROFS` test is reached — and
`EPERM` is what every filesystem returns for an immutable file, so the
distinction is the useful one rather than an accident worth papering
over. The `ro` flag and a read-only provider mount both still produce
`EROFS`.

## 4.5.4.2 Removing directories

The same rule applies, with one additional condition: the **merged**
directory must be empty. A directory is empty for this purpose only if
no participating stratum holds any entry within it, so a directory that
is empty in the provider but not in another participant fails with
`ENOTEMPTY`.

Because determining this reads the contents of every participating
stratum, the caller must hold traverse and list rights on each of them.
That check completes over all participants before any of them is
enumerated, so a refusal yields `EACCES` without disclosing whether the
directory was empty. Without that ordering the emptiness test would be
a disclosure channel: a caller who may not enumerate a protected
participant could learn whether it contains anything by attempting
`rmdir` and distinguishing `ENOTEMPTY` from success.

The emptiness scan filters staging entries, as enumeration does, so an
in-flight copy-up in the create stratum does not make `rmdir` fail on a
directory that looks empty through the mount.

Because it filters them, a directory holding nothing but orphaned
staging entries reports empty — and the removal would then fail at the
provider, on entries the caller cannot see and has no way to remove. So
a scan that finds the directory empty but saw staging entries runs
orphan recovery on the create stratum's copy of it before returning
(§4.5.2). Only entries belonging to no live mount are removed; one owned
by a live mount survives, and the provider's `rmdir` then fails
`ENOTEMPTY`, which is the right answer while another mount is copying up
there.

Where the directory is removed and lower strata hold the same name as a
directory, the name remains visible as a merged directory of the
remaining strata — which, by the emptiness condition, is empty.

## 4.5.4.3 What removal never does

Removal touches exactly one stratum, the provider's. The other strata
are opened read-only for the emptiness scan and nothing else. No entry,
marker, or object is created in any stratum to suppress a lower entry;
no whiteout machinery exists anywhere in the filesystem.

Success is never reported for a name whose provider entry was not
removed. The one place zero is returned without a removal is the
deferred path of §4.5.3, where the entry the descriptor named is
already gone and the deletion is genuinely complete.

---

# 4.5.5 Rename

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> Rename both removes and creates, so the source is bound by the removal constraint — atomic replacement, flags and RENAME_NOREPLACE ordering.

Rename both removes a name and creates one. Because stratafs cannot
make a name disappear (§4.5.4), the constraint on the source is the
same as for removal, and the constraint on the destination follows from
the read-after-write direction of §4.5.1.

For a rename of a source to a destination within one mount, letting `P`
be the source's provider:

1. `P` must accept modification. Otherwise `EROFS` — the source cannot
   be removed, so the rename would leave it still visible and amount to
   a copy.
2. The destination must not be provided by a stratum of **higher**
   precedence than `P`. Otherwise `EROFS` — the renamed object would be
   shadowed at its destination and unreadable through the path it was
   renamed to.
3. `P` must hold the directory containing the destination. Otherwise
   `EXDEV`. That directory is not created to satisfy the condition,
   whether or not `P` is the create stratum: parent materialisation
   exists to receive a copy-up, and a rename is not one.
4. Where the source is a directory, the merged directory must contain
   no entries provided by a stratum other than `P`. Otherwise `EXDEV`,
   since those entries cannot move with it. Determining this reads
   every participating stratum, so traverse and list rights are
   required on each, checked before any enumeration begins; where that
   is refused the rename fails with `EACCES` without disclosing whether
   other strata contributed.
5. Where the destination is provided, its type must match the source's.
   A non-directory onto a directory provider fails with `EISDIR`; a
   directory onto anything else fails with `ENOTDIR`. Only the
   provider's type is compared, since a lower stratum's entry of a
   different type is masked and contributes no inode.
6. Where the destination's provider is a directory, that merged
   directory must contain no entries at all — the same merged-emptiness
   test `rmdir` uses, with the same access requirement. Otherwise
   `ENOTEMPTY`.
7. The rename is performed within `P`: both parents are resolved at the
   provider's index, and a single `vfs_rename` is issued there.

Where the destination is provided by a stratum of lower precedence than
`P` and both are non-directories, the rename succeeds and the renamed
object shadows it. Where the destination is held by `P` itself, it is
replaced, as on any filesystem.

Where the renamed object and the destination are both directories, the
result is not a shadowing: by §4.3.2 the renamed directory merges with
the lower strata's directories of that name. Condition 6 is what keeps
that tolerable — those directories are empty, so the merged result is
the renamed directory's own contents.

`P` need not be the create stratum. A rename is performed in whichever
stratum provides the source, provided that stratum accepts
modification. A rename whose source and destination are in different
mounts fails with `EXDEV`, refused by the VFS before stratafs is
consulted, and stratafs additionally refuses one spanning two mounts
inside the provider stratum.

The immutable-provider caveat of §4.5.4 applies to condition 1 as well:
an immutable source yields `EPERM` from the VFS rather than `EROFS`.

## 4.5.5.1 Replacing atomically

A rename onto an existing destination is the operation by which most
software replaces a file safely, and it works through a stratafs mount
for a destination provided by a lower stratum. That case is permitted
by condition 2: the destination is shadowed by the renamed object
rather than removed, so no whiteout is required, and the replacement is
a single `vfs_rename` within one directory pair of one stratum — atomic
on the filesystem holding `P`.

Where the source of such a rename is a temporary file the caller
created in the same directory, §4.5.3 placed it in the create stratum,
which never carries `ro`. Conditions 1 and 3 are then satisfied.

The contrast with the supersede disposition (§4.5.3) is worth noticing:
that spans two strata and is not atomic.

## 4.5.5.2 Flags

Conditions 1 and 4 concern whether the source can be moved at all, and
apply under every flag. Conditions 2, 3, 5 and 6 concern the
destination.

| Flag | Behaviour |
|---|---|
| `RENAME_NOREPLACE` | The destination must not be held by **any** participating stratum, not merely by `P`. Conditions 2, 5 and 6 are not evaluated, since each presupposes a destination that exists; where any stratum holds the destination the rename fails with `EEXIST`. |
| `RENAME_EXCHANGE` | Both names must be provided by the **same** stratum, and that stratum must accept modification; otherwise `EROFS`. Conditions 1 and 4 apply to both names. Conditions 2, 5 and 6 are not evaluated: an exchange swaps two names that both already exist, so neither type matching nor emptiness is required of either, exactly as on any filesystem. Condition 3 is subsumed. Where either name is a directory, no stratum other than the providing one may hold entries under either name; otherwise `EXDEV`. |
| `RENAME_WHITEOUT` | Fails with `EINVAL`, checked before everything else. stratafs has no whiteouts and cannot represent one. |

Exempting `RENAME_EXCHANGE` from conditions 5 and 6 is what leaves the
flag usable. Its ordinary purpose is to swap two populated directories
atomically, which condition 6 would refuse outright and condition 5
would refuse whenever the two differ in type. The stranded-entries
condition is the only destination constraint an exchange genuinely
needs, because it is the only one that arises from the names spanning
strata rather than from what the names hold.

### 4.5.5.2.1 The `RENAME_NOREPLACE` ordering

A `RENAME_NOREPLACE` whose destination is held by any participating
stratum fails with `EEXIST`, and that `EEXIST` takes precedence over
conditions 1, 3 and 4. It is not stratafs that decides this. For
`RENAME_NOREPLACE` the VFS looks the destination up with `LOOKUP_EXCL`
and returns `EEXIST` for a positive dentry before `vfs_rename` runs at
all, and stratafs's merged lookup makes the dentry positive whenever any
stratum holds the name. `namei.c` is unpatched, so there is no point
inside `->rename` from which the order could be changed.

stratafs nonetheless evaluates conditions 1, 3 and 4 first, and the code
says so. That ordering is reachable only as a race backstop — where the
destination appeared between the VFS lookup and the rename — so a caller
should not expect `EROFS`, `EXDEV` or `EACCES` from a `RENAME_NOREPLACE`
whose destination already existed.

## 4.5.5.3 Other behaviour

Unknown `flags` bits are neither rejected nor masked; they pass through
to the VFS. Where the two provider-level names resolve to one inode the
rename is a no-op returning success. A dentry whose private state is
missing, or whose provider identity no longer matches, yields `ESTALE`.
Because the filesystem sets `FS_RENAME_DOES_D_MOVE`, stratafs performs
the `d_move` or `d_exchange` itself, together with swapping the
dentries' recorded relative paths.

---

# 4.5.6 Links

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> Hard links within one mount, linking an unnamed file, and how symbolic links are created.

## 4.5.6.1 Hard links

To create a hard link from an existing name to a new name within one
mount, letting `P` be the source's provider:

1. `P` must accept modification. Otherwise `EXDEV`.
2. The destination must not be held by any participating stratum.
   Otherwise `EEXIST`.
3. `P` must hold the directory containing the destination. Otherwise
   `EXDEV`. That directory is not created to satisfy the condition,
   whether or not `P` is the create stratum.
4. The link is created in `P`.

`P` need not be the create stratum: a link is made in whichever stratum
provides the source, since that is the only stratum in which the two
names can share an object. Condition 2 ensures the new link is not
shadowed at its destination, since no stratum — above `P` or below it —
holds that name.

Condition 2 is enforced by the VFS rather than by stratafs for a named
source: the link path looks the destination up with `LOOKUP_EXCL`, and
stratafs's merged lookup makes the dentry positive whenever any stratum
holds the name. stratafs's own test covers only the provider stratum
and is a race backstop. For an unnamed source it does check every
stratum explicitly.

A link request is never satisfied by copying the source up. The result
would be a link to the copy rather than to the object named by the
source, so the two names would not share an object — which is the whole
of what a hard link is for. Refusing is the only correct answer.

`EXDEV` is used rather than `EROFS` because it is the error callers
already handle when a link cannot be made between two locations, and
because it is accurate: the link would have to span two strata, which
through this mount are two filesystems.

Where installing the outer inode fails after the lower link succeeded,
the link is rolled back; a failed rollback is audited.

## 4.5.6.2 Linking an unnamed file

An unnamed file created under §4.5.3 is linked into the mount by the
rules for creation, not by those above: it has no provider, so
conditions 1 and 3 have no subject.

The link is created in the create stratum — a source recorded with any
other index is refused with `EXDEV` — and the operation follows §4.5.3:
the name must not be held by any participating stratum, parent
directories are materialised in the create stratum, and a create
stratum that is absent or does not exist fails with `EROFS`.

Linking an unnamed file into a different mount fails with `EXDEV`,
checked both by superblock and by mount, and by the VFS as well.

## 4.5.6.3 Symbolic links

Creating a symbolic link is an ordinary creation and follows §4.5.3:
the link is created in the create stratum, with a security descriptor
established by inheritance there.

A symbolic link's target is stored and returned verbatim. The target
string is passed through untouched on creation, and on read the outer
inode's `get_link` calls the provider inode's own and returns its
result unmodified. No rewriting exists anywhere: a target naming a path
inside a stratum is not rewritten to name the corresponding path inside
the mount, nor the reverse.

Resolution of a symbolic link found in a stratum follows §4.3.1. The
raw string goes back to the VFS, which interprets it as any filesystem's
target is interpreted, so an absolute target resolves from the process's
root and may re-enter this mount, a different stratafs mount, or none.
A link created directly in a stratum by that stratum's owner is
followed as written; the rule constrains only what stratafs itself
does, which is nothing.

---

# 4.5.7 Locking

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> Advisory locks across a stack of independently writable directories — the rule, a changing provider, the retired provider, and leases.

Advisory file locks — whole-file and record locks alike — exist so that
several writers of one file can coordinate. A stratafs mount is
established over directories that have their own writers, so a lock
taken through the mount and a lock taken directly on the same object
must be the same lock.

## 4.5.7.1 The rule

A lock taken through a stratafs mount is held on the provider's object,
in the same lock space as a lock taken on that object by any other
path. Both the POSIX and the `flock` paths retarget the request onto
the descriptor's provider file, so the lock lands on the provider
inode's own lock context.

stratafs maintains no lock space of its own. There is no lock list, no
fallback onto the outer inode, and no per-inode lock state. Two callers
that lock the same provider object — one through the mount, one through
the stratum directly, or two through different stratafs mounts sharing
that stratum — contend with each other. Open-file-description locks are
re-owned onto the provider file so that their per-description semantics
are preserved.

Taking a lock does not modify an object, so it does not route (§4.5.1)
and cannot itself cause a copy-up. Locking requires no write access
either, so a read-only descriptor can carry an exclusive lock.

## 4.5.7.2 Locks and a changing provider

A lock is held on the object a descriptor resolved to. That object may
cease to be the provider afterwards — because a higher-precedence
stratum gains the name, because the object is removed from its stratum,
or because a copy-up produced a new object.

In every such case the lock remains held on the object it was taken on.
It is not transferred, and it does not begin to guard the new provider.

**Two callers may therefore hold exclusive locks on one merged path
without contending**, whenever they opened it either side of a change of
provider. Neither is wrong about the object it locked; they locked
different objects, and each lock is honoured by everything else holding
that object. The merged path is what stopped naming one thing. §4.8
records it.

A caller that must be sure it holds a lock on the current provider has
to reopen and re-take it, which is the same discipline required of
anything that locks a path another process may replace by rename. The
exposure here is that a copy-up is a replacement the caller did not
perform and cannot see.

## 4.5.7.3 The retired provider

Copy-up does not close the file it copied from. The pre-copy-up
provider file is moved aside into the descriptor's private state
specifically to keep locks and leases taken before the copy-up alive,
and the fan-out that follows is invisible to the specification but
visible in behaviour:

- A POSIX or `flock` **unlock** is applied to both the retired file and
  the copy; a non-unlock request goes only to the copy.
- A lease release is applied to both. A lease *acquisition* is
  redirected to the retired file when that file already holds a lease
  of the same flavour.
- Querying a lease returns the stronger of the two files' lease types,
  ordering write above read above none.
- Closing the descriptor runs the underlying flush and removes POSIX
  locks on both files, and releasing it breaks leases on both, using
  the outer file as the owner identity.

All of it is serialised by the descriptor's mutation lock.

## 4.5.7.4 Leases and mandatory locking

Leases are established on the provider's object, in that object's own
lock space, and every result is the provider's own, returned unchanged.
stratafs neither adds to nor removes from whatever semantics the
provider's filesystem gives them, and never reports a lock as
established where the provider's filesystem refused it.

Mandatory locking has no subject here: the platform does not offer it,
and stratafs contains nothing that would obstruct it. The outer inode
copies the provider's inode flags wholesale.

## 4.5.7.5 Internal locks

The specification names no internal lock and fixes no acquisition
order. The implementation's hierarchy, outermost first:

1. The per-open-file **mutation lock**, taken by every operation that
   may copy up.
2. The per-outer-inode **rebind lock**, taken inside it whenever an
   inode's provider is swapped.
3. The dentry lock or the inode lock, taken inside the rebind lock and
   never overlapping each other.

The superblock's identity lock and staging lock are each taken alone.
`copy_file_range` and `remap_file_range` deliberately do not nest the
two files' mutation locks: the input file's is taken, a reference
grabbed, and released before the output file's is taken.

For mutations the ordering is: the KACS decision, then parent
materialisation, then write access on the target mount, then the
parent's directory lock through the VFS's create, remove or rename
helpers. Refusal auditing takes the dentry lock with an atomic
allocation first and falls back to the rebind lock only if that fails,
so the two are never nested.

---

# 4.5.8 Durability and Accounting

_Peios / Advanced Peios / PKM / stratafs / Mutation_

> stratafs holds no storage, so durability and accounting belong to the providers — except where a merged directory needs a rule of its own.

stratafs holds no storage, so durability is the providers' and the
accounting is theirs too. Two operations nonetheless need a rule,
because a merged directory has no single provider to forward to.

## 4.5.8.1 Synchronising an object

Synchronising a non-directory is forwarded to the object the descriptor
resolved to — the descriptor's own provider file — and reports what
that object's filesystem reports. No path re-resolution happens, so it
is never forwarded to whichever object currently provides the path.
Where the two differ, the data the caller wrote is on the object it
opened, and synchronising anything else would report success while
leaving that data unsynchronised.

Where a copy-up has occurred through this descriptor, the descriptor's
object is the copy, and it is the copy that is synchronised. The
retired pre-copy-up file (§4.5.7) is not.

## 4.5.8.2 Synchronising a merged directory

Synchronising a merged directory synchronises the corresponding
directory in **every** stratum of the mount that holds it at the time
of the call, and fails if any of them fails. The loop continues past a
failure, so every stratum is still attempted, and the first error is
what is returned.

The set is evaluated when the operation runs, not when the descriptor
was opened: the directory is re-resolved across all strata rather than
read from the participant set settled at open (§4.3.4). Both halves of
that matter:

- Evaluating at call time catches a directory the create stratum did
  not hold when the descriptor was opened and does now — which is
  exactly what happens when a file is created through that descriptor
  and §4.5.3 materialises its parent.
- Covering every stratum rather than the provider alone is required by
  the atomic-replace pattern of §4.5.5, whose rename is performed in
  the stratum that provided the *source*, which need not be the stratum
  providing the merged directory.

Durability is a question about what is on disk now, which is why this
is the one place a merged directory is treated as its current set of
real directories rather than as the thing a descriptor was opened
against.

## 4.5.8.3 Freezing

A stratafs mount has no storage to quiesce. Freezing returns
`EOPNOTSUPP` and propagates nothing to any stratum's filesystem; no
unfreeze, freeze-super or thaw-super operation is registered at all.
Freezing the filesystem a stratum lives on is done through that
filesystem, and affects the merged view as it affects any other reader
of that stratum.

## 4.5.8.4 Accounting

Storage consumed by an object created through the mount, or copied up
into the create stratum, is consumed on the create stratum's filesystem
and accounted there.

Which principal it is accounted to is not what the specification
describes. Disk quota keys on the POSIX owner, and copy-up does not
preserve it: the staged object is created with the calling task's
credentials, and the metadata copy transfers only the mode and the
modification time. A copy is therefore accounted to the caller who
caused it, not to the owner of the object it was copied from.

What *is* preserved is the KACS security descriptor, including its
owner SID (§4.6.3), so the descriptor-level owner is the source's. The
two notions of owner diverge here, and only the descriptor one behaves
as §4.6.3 requires. This is tracked as a defect.

No code alters ownership to redirect accounting; the divergence is one
of omission. The audit record of §4.6.5 is where the causing caller is
recorded.

---

# 4.6.1 Access Check Delegation

_Peios / Advanced Peios / PKM / stratafs / Security_

> stratafs stores no descriptors and allocates its inodes bare — who performs which access check, and when.

stratafs stores no security descriptors. It allocates its outer inodes
bare and never runs the inode security initialisation over them, and
neither its inode state nor its superblock state has anywhere to put a
descriptor. Every object reachable through a mount has its descriptor
on its own stratum, and that descriptor is what governs access to it.

## 4.6.1.1 The rule

An access check for an operation on an object reachable through a
stratafs mount evaluates the security descriptor of the object the
operation will be performed against. For an operation on a
non-directory, that is the provider's object; for a merged directory,
§4.6.2 defines which participants' descriptors apply.

stratafs synthesises no descriptor, supplies none of its own, and
applies no mount-level template. It could not: constructing one would
require a synthesising mount policy class, and stratafs is pinned to
the class that denies where a descriptor is missing (§4.6.4).

Because the descriptor evaluated is the provider's own, a stratafs
mount cannot grant access that the provider's stratum would refuse.
That property is structural rather than a matter of care in
implementation: there is no descriptor for stratafs to get wrong,
because it holds none.

The guarantee is one-directional, which is the direction that matters.
Where the provider carries no descriptor there is nothing to evaluate
and the mount's own policy decides, which is to refuse — so such an
object is unreachable through the mount even where its own filesystem's
policy would have admitted it.

## 4.6.1.2 Who performs which check

For an object with a single provider, the descriptor comes back through
ordinary forwarding. KACS reads the canonical descriptor attribute the
same way it does for any file, the stacking layer forwards the
`getxattr` down to the provider, and the descriptor that comes back is
the provider's. For metadata and extended-attribute operations, stratafs
re-targets the pending one-shot KACS decision onto the provider inode,
so the check is made against the object the operation will reach.

A merged directory is not that case. It stands for several directories
with several descriptors, and forwarding yields only the provider's.
Those checks are **stratafs's own**: it walks every present
participating directory and evaluates each one's descriptor, failing on
the first refusal.

KACS cooperates by standing down on stratafs inodes entirely. Its
`inode_permission` hook, and its create, mkdir, mknod, symlink, link,
unlink, rmdir and rename hooks, all return success immediately for a
superblock carrying the stratafs magic. The checks that matter are made
by stratafs against the real objects, or by KACS against the real
objects once stratafs has resolved them.

## 4.6.1.3 Timing

The descriptor evaluated is the provider's at the time the check runs,
and the open-time grant is frozen into the file's KACS state — the
check-at-open principle applies unchanged, and copy-up transfers that
immutable snapshot to the new backing file rather than deciding again.

Two caching seams are worth recording precisely, because the
specification requires the value evaluated to be the provider's
*current* descriptor.

For the merged-directory checks stratafs performs itself, it is exact:
the provider's attribute is re-read on every call, with no cache.

For file opens it is not. KACS caches the resolved descriptor against
the **outer stratafs inode**, and a cache entry sourced from an
attribute read is never revalidated — the only things that replace it
are an explicit descriptor set on that inode and a copy-up install. A
later open of the same outer inode therefore evaluates the descriptor
read at the first open rather than the provider's current one. This is
tracked as a defect.

The second seam is narrower. When copy-up rebinds a descriptor's inode
to the copy (§4.4.3), nothing invalidates that cached descriptor, so
the value retained was literally read from the old provider. No wrong
decision follows, because copy-up preserves the descriptor exactly
(§4.6.3) and the two are equal — but the invariant is held by that
coincidence rather than by the mechanism.

---

# 4.6.2 Checks on Merged Directories

_Peios / Advanced Peios / PKM / stratafs / Security_

> An operation on a merged directory is checked against every participating directory — including where the create stratum's directory does not exist.

A merged directory stands for several real directories, each with its
own security descriptor. An operation on one is checked against every
participating directory whose contents it depends on, and against the
directory it modifies. Where more than one applies, **all** must
succeed: the check walks participants in order and returns on the first
refusal, and nothing degrades an operation to the subset of strata the
caller may reach.

Two composite rights recur:

- **Search** — traverse on every participating directory, since every
  one is searched to resolve a name. The permission hook maps the
  kernel's execute intent onto it.
- **Enumerate** — Search plus list on every participating directory.
  The permission hook maps the read intent onto the list right, and
  directory open demands both together.

| Operation | Checked against |
|---|---|
| Resolving a name | Search |
| Enumerating | Enumerate, before the listing is captured |
| Reading an object's attributes | Search, plus whatever the object's own descriptor requires |
| Reading the origin attribute on a merged directory | Search, plus read-EA on **every** participant (§4.7) |
| Creating a name | Search, plus add-file or add-subdirectory on the **create stratum's** directory |
| Copy-up | Nothing beyond what the operation it serves already required |
| Removing a name | Search, plus delete-child on the **provider's** directory |
| Removing a directory | Enumerate — emptiness is judged across every participant — plus delete-child on the provider's directory |
| Rename, source | Search, plus delete-child on the source **provider's** directory |
| Rename, destination | Search, plus add-entry on the source provider's directory, which §4.5.5 requires to hold the destination, plus delete-child where that directory already holds the name |
| Rename of a directory | Both of the above, plus Enumerate on the object being renamed, and Enumerate on the destination where its provider is a directory |
| `RENAME_EXCHANGE` | Search on both, plus add-entry and delete-child on the directory of the stratum providing both names |
| Link | Search on both, plus add-file on the source provider's directory |
| Creating or linking an unnamed file | Search, plus add-file on the **create stratum's** directory |

The mutating rights land on the directory that actually changes, which
follows from §4.5: an entry appears in the create stratum's directory
and disappears from the provider's, so in each case it is that
directory's descriptor that decides. The two coincide whenever the
create stratum is also the provider.

One right the table does not name is required anyway: the underlying
`vfs_link` makes KACS demand write-attributes on the **source object**,
which is an object-descriptor right outside the directory scope this
section covers. Linking an unnamed file is exempt, since stratafs marks
the source for that purpose.

The effective rights on a merged directory are therefore the
intersection of the rights on its participants, with mutating rights
additionally required on the directory that is actually modified.
Requiring the intersection is fail-closed and admits no partial
results: the alternative would let the names held by a restrictively
protected directory be enumerated by a caller who could not enumerate
it directly, because a permissive directory of higher precedence
happened to provide the merged path.

One consequence is worth stating plainly. Because creation is governed
by the create stratum's directory descriptor, and a created name
shadows the same name in every lower stratum, the right to create in
the create stratum's directory is the right to determine what every
lower stratum's entry of that name resolves to.

## 4.6.2.1 Where the create stratum's directory does not exist

A merged directory has a create stratum whether or not that
subdirectory exists (§4.3.2), so a check naming "the create stratum's
directory" has to be evaluated against a directory that is not there
yet.

Every such check is performed before any part of the operation is
carried out, and in particular before any directory is materialised:
the authorisation call strictly precedes parent materialisation in
creation, in tmpfile creation, and in the unnamed-link path. Where the
checks fail, nothing has been created, so the create stratum is left
exactly as it was.

Where the create stratum does not hold the path, the check falls back
to the **corresponding provider directory** — the merged provider of
that same relative path — which is the descriptor the directory will
carry once materialised (§4.6.3). It is never skipped, and never
substituted with an ancestor's descriptor. Where no provider exists
either, the result is `EROFS`.

Materialising the intervening directories is part of the operation, not
a separate one. No per-ancestor authorisation is taken; the mkdirs are
exempted by the copy-up context. Checking against the descriptor the
directory will have keeps the answer independent of how much of the
create stratum happens to have been materialised already: the first
caller to write into a deep path and the hundredth face the same check,
against the same descriptor.

A create that fails *after* materialisation for a reason other than a
check — `EEXIST`, `ENOSPC` — does leave the intervening directories in
place.

## 4.6.2.2 Copy-up carries no separate authority

A copy-up requires no right beyond those the operation that provoked it
already required. In particular it does not require the caller to hold
the right to read the provider's object, nor the right to add an entry
to the create stratum's directory. Neither copy-up path takes any
authorisation at all.

Copy-up is the mechanism by which an authorised modification is
realised, not an operation a caller requests. The read of the provider
is stratafs's own, and the copy carries the source's descriptor
unchanged (§4.6.3), so the caller obtains nothing they did not already
have: the same content, under the same descriptor, at the same path.

The alternative cannot be expressed. Routing happens when a
modification is performed (§4.5.1), and by then the authority that
governed the open is a mask cached on the descriptor, immutable and
carrying no token — so there is nothing to evaluate a fresh right
against. Checking the *acting* token instead would break descriptor
delegation, since a descriptor passed to another process would stop
working there.

What remains is a resource consideration rather than an access one: a
caller entitled to write a file in a stratum that will not accept
modification can cause an entry to appear in the create stratum without
holding rights over that directory. They gain no access by it — though
they do gain the space, since §4.5.8 records that the copy is accounted
to them rather than to the preserved owner.

That exemption is only enforceable because KACS provides a copy-up
context; without it the access-control layer would check the caller
against the create stratum's directory at exactly the point where the
authority to check no longer exists. §3.9.7 describes the context, its
phase binding and its exhaustive list of exempt operations. What
matters here is what stratafs must hold up its end of: the context
exempts caller authorisation only, so every mutation still goes through
the ordinary VFS path under write access on the target mount, a
read-only filesystem still refuses, and filesystem errors still
propagate. Nothing is performed under a borrowed or elevated identity —
every copy-up mutation runs with the calling task's own credentials.

One adjacent path does borrow one. The stale-staging recovery scan
opens the create-stratum directory with the **mounter's** credentials
rather than the caller's, and since the KACS token derives from the
current credentials, that read is authorised as the mounter. It runs
inside a copy-up context in any case, so it changes no decision.

---

# 4.6.3 Descriptors on Copy-Up

_Peios / Advanced Peios / PKM / stratafs / Security_

> A copy-up carries the source's descriptor rather than inheriting a new one — how it is carried, what failure means, and why preservation wins.

Copy-up produces a second instance of an existing object. Its security
descriptor is the source's — owner, group, discretionary list, system
list and integrity label alike — and it is KACS that carries it, not
stratafs.

## 4.6.3.1 How it is carried

When a copy-up context is created, the provider's complete effective
descriptor is resolved and pinned as a byte string on the context. Each
create phase copies those bytes into the phase, and the inode security
initialisation for the created object installs them verbatim instead of
running inheritance. Nothing is reconstructed field by field: it is a
copy of the source bytes, so all five components are preserved together.

The canonical descriptor attribute is deliberately excluded from
stratafs's own extended-attribute replication, precisely so that the
two mechanisms cannot disagree.

The separation inside KACS is clean and explicit. The inode security
initialisation takes the copy-up branch first, and taking it
short-circuits the inheritance builder entirely. Ordinary creation in a
create stratum inherits from its parent (§4.5.3); copy-up preserves.
There is no path on which a copied-up object receives an inherited
descriptor.

Directories materialised in the create stratum to hold a copied-up
object are handled the same way, each carrying the descriptor of the
corresponding **provider** directory — the merged provider of that same
relative path, which is what the pre-check of §4.6.2 evaluated against.

## 4.6.3.2 Failure

Where the source descriptor cannot be replicated, the copy-up fails and
the operation that required it fails. A missing, corrupt, unresolvable,
oversized or unsupported descriptor fails the phase before the
destination is created, and an absent pinned descriptor at install time
fails the create. Nothing publishes a copied-up object carrying any
other descriptor: the descriptor is installed at inode creation, before
any content, and publication is a link or rename of an object already
stamped, so there is no window in which a differently-protected object
is reachable.

The failure errno is not the specified one. §4.8's table pairs
descriptor failure with `EIO` alongside extended-attribute failure, and
the extended-attribute half does report `EIO`. The descriptor half is
carried by KACS, whose failures surface as `EACCES`, `EOPNOTSUPP`,
`EINVAL`, `ESTALE` or `ENOMEM`; there is no `EIO` anywhere on that
path. This is tracked as a defect.

## 4.6.3.3 Why preservation rather than inheritance

Copy-up is reachable by any caller with the right to modify the source
object. That right does not include the right to modify the source's
descriptor, which is a separate right.

Were a copied-up object to receive an inherited descriptor, a caller
holding only write access to a restrictively protected object could
cause a copy of it to exist carrying the create stratum directory's
inheritable entries — and that copy, having higher precedence, would
become what the merged path resolves to. The object's confinement would
have been replaced by the directory's, through an operation the caller
was entitled to perform, without their ever holding the right to alter
a descriptor.

Preserving the source descriptor closes that: a copy is exactly as
reachable as its original, and copy-up changes which stratum holds an
object without changing who may reach it.

## 4.6.3.4 Provenance

Because the descriptor is preserved, the copy's descriptor-level owner
is the owner of the object it was copied from, not the caller who
caused the copy. Ownership therefore does not record who created the
copy and cannot be relied on to; the audit record of §4.6.5 is where
that is available.

Setting the copying caller as the descriptor owner would have preserved
provenance, and was rejected: an owner holds implicit rights over an
object's descriptor, so making the caller the owner would reintroduce
the escalation this section exists to prevent by a slightly longer
route.

The POSIX owner is a different matter, and is *not* preserved — §4.5.8
records what follows.

## 4.6.3.5 What "the source's descriptor" means

The descriptor pinned at the start of a copy-up is the provider's
**effective** descriptor rather than its raw stored attribute. On a
provider mount whose own policy class synthesises, that would be the
synthesised value. In practice that case is unreachable: reaching the
object through the stratafs mount at all requires a real descriptor,
under stratafs's own deny-missing class (§4.6.4). A provider on an
unmanaged mount is refused outright.

---

# 4.6.4 Mount Policy

_Peios / Advanced Peios / PKM / stratafs / Security_

> Every stratafs mount carries the FACS policy class that denies access to an object with no readable descriptor, and what follows from that.

A stratafs mount carries the FACS mount policy class that denies access
to an object with no readable security descriptor. The class is derived
from the filesystem magic, not from an administrative choice: the
policy resolver maps the stratafs magic to the deny-missing class, and
falls back to that mapping whenever a cached policy value is not one of
the valid ones.

It cannot be set to anything else. The set path rejects a superblock
carrying the stratafs magic with `EOPNOTSUPP` **before** it validates
its arguments or checks privilege, and there is no mount option to
choose one — stratafs's parameter table holds exactly one entry, and
anything else is refused. Reading a mount's policy is itself privileged,
requiring TCB privilege, though for stratafs the answer is fixed.

Two classes are excluded for distinct reasons.

**The unmanaged class** declares that FACS does not apply to a mount and
that the kernel governs it by rules particular to that filesystem.
stratafs has no such rules: it delegates every decision to the provider
(§4.6.1). An unmanaged stratafs mount would therefore have no access
control at all — not delegated control, but none — for every object
reachable through it. That is not theoretical: enforcement points
consult the mount policy of the superblock an object belongs to before
performing their check, and treat an unmanaged mount as requiring none.
For a mount established over a directory of executables, that would
place every program on the system beyond the execute check.

**The synthesising classes** would have stratafs supply a descriptor of
its own for an object whose provider has none. Either consequence is
disqualifying: the mount would grant access to an object that is
unreachable through its own stratum, falsifying the guarantee the whole
of §4.6 rests on; and the class that persists a synthesised descriptor
would write it back onto the provider's object, which stratafs may not
do to any stratum and certainly not to one carrying `ro`.

## 4.6.4.1 Missing descriptors

Where a provider object has no descriptor, access through the stratafs
mount is denied under the mount's own policy, and the provider
filesystem's policy is not consulted. Both paths implement this:
stratafs's own merged-directory check turns `ENODATA` or `EOPNOTSUPP`
straight into `EACCES` rather than asking the provider's superblock to
synthesise, and an object open resolves the missing-descriptor policy
against the **stratafs** superblock, yielding a missing-descriptor
cache entry and then `EACCES`.

A descriptor that is present but cannot be interpreted is a distinct
case and is not routed to mount policy. It takes the ordinary
corrupt-descriptor outcome: a corrupt cache entry and an emitted event
on the open path, and a validation failure on the merged-directory
path. The two produce the same errno by different routes, and mount
policy is consulted in neither.

## 4.6.4.2 Consequence

A stratafs mount is uniform in the sense a mount policy requires: every
object reached through it is subject to the same policy, which is the
mount's own. What varies between objects is the descriptor evaluated,
which is a property of the object rather than of the policy.

Because the policy denies where a descriptor is absent, the divergence
from direct access is always in the refusing direction. An object with
no descriptor on a stratum whose own filesystem would synthesise one is
refused through the stratafs mount while remaining reachable through
its stratum path. An object reachable through its stratum path is never
made *more* reachable by being merged. The same holds for a stratum on
an unmanaged filesystem, whose objects carry no descriptors at all:
merging one is permitted but yields nothing readable, and is not a
useful arrangement.

---

# 4.6.5 Audit

_Peios / Advanced Peios / PKM / stratafs / Security_

> The two stratafs events recorded because the information cannot be recovered afterwards — plus two gaps and what is not audited at all.

Two classes of stratafs event carry information that cannot be
recovered from the filesystem afterwards, and are recorded when they
happen. Both are emitted through KACS's kernel-only emitter, so KMES
stamps each with the effective token of the task whose operation caused
it.

## 4.6.5.1 Copy-up

Every copy-up emits a record, successful or not. The payload is a map
of six keys:

| Key | |
|---|---|
| `path` | The relative path within the mount, `/`-prefixed |
| `provider_index` | The stratum the object was copied from |
| `provider_stratum` | That stratum's path |
| `create_index` | The stratum it was copied into |
| `create_stratum` | That stratum's path |
| `result_errno` | Zero on success, the failure otherwise |

The caller's identity is not in this payload. It does not need to be:
KMES stamps the effective, true and process token GUIDs onto every event
header at ring-write time, and because copy-up runs in the caller's own
context those are the caller's. The identity is carried in the
**envelope**, once, for every event — duplicating it into the payload
would give a reader a second copy that could disagree with the first.

Recording it matters because §4.6.3 preserves the source's descriptor,
so nothing about the resulting object records who caused it to exist.
A reader of these records must take the identity from the event header,
not look for it among the keys.

The `ENOTDIR` of parent materialisation is reported through this event
with its `result_errno` rather than through the refusal event below;
the required fields are all present, under a different name.

## 4.6.5.2 Refused mutation

A mutation refused because of how the mount is arranged emits a record
with six keys: the path, the operation name, the provider index, the
provider stratum path, the errno, and whether the refusal was deferred.
The provider stratum is a string where a provider is known and msgpack
nil where none is; see below.

What counts as an arrangement refusal is one explicit list — `EROFS`,
`EXDEV`, `ENOTDIR`, `EISDIR`, `ENOTEMPTY`, `EEXIST`, `EINVAL`. Call
sites cover
every mutating path: writes, mappings, truncation, `fallocate`, splice,
`copy_file_range`, `remap_file_range`, `setattr`, `setxattr` and
`removexattr`, creation, tmpfile, unlink and rmdir, link, supersede,
and rename.

`EACCES` is deliberately absent from that list. A refusal produced by an
access check is audited by the mechanism that performed it, and
stratafs does not duplicate those records.

These refusals report a mismatch between what a caller attempted and
how the mount is arranged — software writing where it cannot, or an
arrangement that does not admit an operation someone expected. That is
diagnostic information about the system's configuration, and it is
otherwise visible only as an error returned to a caller that may
discard it.

Rollbacks are audited under the same event with the deferred flag set:
a create or link whose outer bookkeeping failed and whose lower object
could not be removed again, and a failed publication rollback after a
copy-up.

A refusal raised before a provider is known — creation, tmpfile, the
heads of link and rename — has no stratum to name. It reports a provider
index of `-1` and a `provider_stratum` of msgpack **nil**, so a reader
can tell "no provider was involved" from "the provider's path is empty".
The two fields agree: an index of `-1` always accompanies a nil stratum.

### 4.6.5.2.1 One exception

A refused **deferred deletion** is audited on *any* non-zero result, not
only the arrangement errors, so one refused by an access check does get
a stratafs record. That is the right resolution of the two rules, since
the requirement to audit a deferred deletion is unconditional — nobody
is left to receive the error — but it is a deliberate exception to the
`EACCES` exclusion above.

## 4.6.5.3 What is not audited

Resolution, revalidation and enumeration emit no records of their own.
They occur on every path operation, they reveal nothing the resulting
access check does not, and recording them would produce volume out of
all proportion to their significance. There is no audit call anywhere
in the lookup path.

Access checks performed against provider objects are audited by KACS,
under its own rules.

---

# 4.7 The Origin Attribute

_Peios / Advanced Peios / PKM / stratafs_

> The one synthetic extended attribute that reveals which stratum provided a merged path — its value, constraints and access rules.

A merged path does not reveal which stratum provided it. stratafs
exposes that through one synthetic extended attribute,
`system.stratafs.origin`, and deliberately through nothing else — a
tool that instead re-implemented §4.3's resolution rules against the
strata would be a second implementation of them, and would eventually
disagree with the first.

Together with the mount table (§4.2.2), which gives the stratum stack,
it is enough to explain any path in a mount without privilege beyond
what reading the path itself requires.

## 4.7.1 The value

Reading the attribute returns the absolute path of the object that
provides it:

- For a **non-directory**, the provider's path in its stratum.
- For a **merged directory**, the paths of every participating
  directory in precedence order, separated by newlines.

Each element is the stratum's own path, then a `/` where the relative
part is non-empty and the base does not already end in one, then the
relative path. Within a path, a newline or a backslash is escaped by a
preceding backslash; those two are the whole escape set, and the `/`
stratafs itself inserts is not escaped. Stratum paths are absolute
because the mount parser required them to be (§4.2.2).

There is no trailing newline and no trailing NUL. A null buffer returns
the length required; an undersized one returns `ERANGE`.

The value is synthesised at each read from the current resolution. It
is not stored, and it is not the value of any attribute on any stratum.
Because non-root dentries are always invalidated (§4.4.2), a read by
path always reflects a fresh resolution.

## 4.7.2 Constraints

The attribute is not settable. Any set or removal in the reserved
namespace fails with `EPERM`, before the provider is reached at all.

It is not reported by an attribute listing. The listing handler filters
reserved names out of both its sizing pass and its copy pass, and the
synthetic name is never added to any listing. Hiding it keeps
archivers, copy tools and backup software from discovering it,
attempting to preserve it, and failing.

The whole `system.stratafs.` namespace is reserved. No read, write or
removal of any name in it is forwarded to the provider: a read of any
name other than `origin` returns `ENODATA`, and a write or removal
returns `EPERM`. Where a provider object carries a real attribute of
one of these names, it is masked — the synthesised value is returned
instead, and the provider's attribute is absent from listings through
the mount.

Two details of the implementation are worth stating exactly. The
namespace test is a fixed-length prefix comparison against
`system.stratafs.` including the trailing dot, so the bare name
`system.stratafs` is *not* reserved and would be forwarded to the
provider. And the reserved set is one name wider than the namespace
suggests: the staging marker attribute,
`security.peios.stratafs_staging`, receives the same treatment —
`EPERM` to write, `ENODATA` to read, hidden from listings, masked from
providers — despite lying outside the `system.stratafs.` namespace.

## 4.7.3 Access

Reading the attribute requires the access that reading an extended
attribute of the object requires, and is refused where that would be
refused. The right to read the object's stat attributes is not
sufficient: the request is for the read-EA right, which KACS's
`getxattr` hook demands on the stratafs dentry before stratafs's own
handler runs.

For a merged directory the value names every participating directory,
so it discloses more than any one of them. Reading it requires that
access on **every** participating directory and is refused where any
refuses — the same intersection §4.6.2 applies to enumeration, and for
the same reason: a caller who may not know a restricted directory
participates must not learn it from this attribute.

Where the attribute is read **through a directory descriptor**, the
participating set is the one settled when that descriptor was opened
(§4.3.4), and the value names that set rather than the current one. The
access decision was likewise made at open and is recorded on the
dentry, so the read consults a stored verdict rather than re-checking.
A stratum that has joined since is not disclosed, because the check
that would have covered it was never run; a settled participant that
has since ceased to hold the directory is still named, for the same
reason.

Where it is read **by path**, the participating set is the current one,
resolved afresh, and the read-EA right is required against each of its
members at that moment.

---

# 4.8 Failure Modes

_Peios / Advanced Peios / PKM / stratafs_

> Every condition under which a stratafs operation fails and the error it produces, from mount time through resolution and mutation.

This section consolidates the conditions under which a stratafs
operation fails and the error each produces. The sections above remain
authoritative for the conditions themselves.

## 4.8.1 Mount-time

| Condition | Error |
|---|---|
| Caller lacks the access to resolve a stratum path or read its attributes | `EACCES` |
| Empty stratum stack, or `strata=` absent | `EINVAL` |
| Stack longer than 16 strata | `EINVAL` |
| More than one stratum carries `create` | `EINVAL` |
| A stratum carries both `create` and `ro` | `EINVAL` |
| The same directory appears twice in the stack | `EINVAL` |
| Any malformed `strata=` value (§4.2.2) | `EINVAL` |
| `strata=` supplied on a remount | `EINVAL` |
| An allocation failure while parsing | `ENOMEM` |
| A create-bearing stack established outside the initial user namespace, or without `CAP_SYS_ADMIN` there | `EPERM` |
| A stratum path names a non-directory | `ENOTDIR` |
| A stratum is absent without `am` | `ENOENT` |
| A stratum lies within the mount point, or within another stratafs mount whose strata include this mount point | `ELOOP` |
| The composed stack reaches the kernel's maximum stacking depth | `ELOOP` |
| Sixteen consecutive collisions allocating a mount cookie | `EAGAIN` |

The option-only conditions are decided before any path is touched, and
the `EPERM` admission test before any path is resolved. The per-path
conditions follow. §4.2.3 records the one place the specified ordering
is not achieved.

## 4.8.2 Resolution

| Condition | Error |
|---|---|
| No participating stratum holds the name | `ENOENT` |
| An ancestor of the path is masked by a non-directory provider | `ENOTDIR` |
| Operation requires a non-directory; provider is a directory | `EISDIR` |
| Traverse or list refused on any participating directory | `EACCES` |
| A stratum walk fails for a reason other than absence | that error |
| A joined stratum path or child relative path exceeds `PATH_MAX` | `ENAMETOOLONG` |
| A task re-enters the same superblock while resolving | `ELOOP` |

## 4.8.3 Mutation

| Condition | Error |
|---|---|
| Provider will not accept modification, and no create stratum has higher precedence | `EROFS` |
| Any mutation on a read-only-mounted stratafs | `EROFS` |
| Creation, unnamed-file creation, or unnamed-file link with no create stratum, or it absent | `EROFS` |
| Create stratum holds a path component as a non-directory | `ENOTDIR` |
| Exclusive creation where any stratum holds the name | `EEXIST` |
| Copy-up cannot preserve an extended attribute | `EIO` |
| Copy-up cannot preserve the descriptor | `EACCES`, `EOPNOTSUPP`, `EINVAL`, `ESTALE` or `ENOMEM`, as KACS reported it |
| Copy-up for a descriptor whose object is no longer the provider, or whose target name is taken at publication | `ESTALE` |
| Copy-up of a device node, FIFO or socket | `EROFS` |
| Unlink where the provider will not accept modification | `EROFS`, or `EPERM` where the provider inode is immutable |
| Rmdir where another stratum holds entries within | `ENOTEMPTY` |
| Rmdir or directory rename where list is refused on a participant | `EACCES` |
| Modifying a special file's mode, descriptor or extended attributes where its provider will not accept modification | `EROFS` |
| Establishing a shared write-capable mapping where routing yields read-only | `EROFS` |
| Replacing disposition where §4.5.4 would refuse the removal, or where a stratum above the create stratum would still hold the name | `EROFS` |
| Replacing disposition on anything but a regular file | `EOPNOTSUPP` |
| Rename where the source's provider will not accept modification | `EROFS` |
| Rename where a stratum above the source's provider provides the destination | `EROFS` |
| Rename where the source's provider does not hold the destination's directory | `EXDEV` |
| Rename of a directory containing entries from other strata | `EXDEV` |
| Rename of a non-directory onto a directory provider | `EISDIR` |
| Rename of a directory onto a non-directory provider | `ENOTDIR` |
| Rename onto a destination directory not empty across every stratum | `ENOTEMPTY` |
| `RENAME_NOREPLACE` where any stratum holds the destination | `EEXIST` |
| `RENAME_EXCHANGE` where the two names are not provided by one stratum that accepts modification | `EROFS` |
| `RENAME_EXCHANGE` where another stratum holds entries under either directory name | `EXDEV` |
| `RENAME_WHITEOUT` | `EINVAL` |
| Hard link whose source's provider will not accept modification, or does not hold the destination's directory | `EXDEV` |
| Hard link where any stratum holds the destination | `EEXIST` |
| Linking an unnamed file into a different mount | `EXDEV` |
| A dentry whose private state is missing, or whose provider identity no longer matches | `ESTALE` |
| `ioctl` on a regular file | `ENOTTY`, or `ENOIOCTLCMD` for the compat form |
| `remap_file_range` with flags outside dedupe and advisory | `EINVAL` |

## 4.8.4 Interface

| Condition | Error |
|---|---|
| Set or remove of any reserved attribute | `EPERM` |
| Read of a reserved attribute other than `origin` | `ENODATA` |
| Origin read where read-EA is refused on any participant | `EACCES` |
| Origin read into an undersized buffer | `ERANGE` |
| Freezing the filesystem | `EOPNOTSUPP` |
| Reading a mount's policy without TCB privilege | refused by KACS |

## 4.8.5 Intended divergences

None of the following is a defect. Each is described above, and each
follows from stratafs having no way to record that a name should be
absent, or from a merged path standing for objects in more than one
stratum.

- **Removing a name may not remove it from the view.** Where a lower
  stratum also holds the name, it becomes the provider and the name
  remains, now resolving to different content (§4.5.4).
- **A name may be unremovable.** Where the provider will not accept
  modification, removal fails however the caller is privileged
  (§4.5.4).
- **Rename may be refused for an unmodified file.** Where the source's
  provider will not accept modification, rename fails rather than
  silently copying (§4.5.5).
- **Hard links may be refused within one directory.** Where the
  source's provider will not accept modification, linking fails with
  `EXDEV` (§4.5.6).
- **An object's inode number may change.** Where the provider for a
  path changes, a new inode is presented (§4.4.3).
- **Two callers may hold non-contending locks on one path.** Locks are
  held on the object a descriptor resolved to, so callers who opened
  either side of a change of provider — including one caused by a
  copy-up neither of them performed — have locked different objects
  (§4.5.7).
- **Copy-up severs hard links.** Two names that shared an object in a
  lower stratum, and compared equal by inode number, refer to different
  objects once one of them is written through the mount; only the
  written one is copied up (§4.5.2).
- **An open may succeed where the first write then fails.** Routing
  happens when a modifying operation is performed, not at open, so an
  open for writing against a provider that will not accept modification
  succeeds and the `EROFS` arrives at the write, the truncate, or the
  attempt to establish a shared writable mapping (§4.5.1).
- **A copy-up moves an object out from under descriptors already open
  on it.** Only the descriptor whose operation caused the copy-up refers
  to the copy; others continue to refer to the original, and locks held
  on it stay there (§4.5.1, §4.5.7).
- **A write may fail `ESTALE` on a descriptor that was valid when
  opened.** Where another descriptor, another mount, or a direct writer
  has since caused that name to be provided by a different object, a
  copy-up cannot proceed and the caller must reopen (§4.5.2).
- **`fstat` and `stat` may report different inode numbers for one
  file.** After a copy-up, the descriptor that caused it keeps the inode
  it was opened against while a fresh resolution of the path yields a
  new one, so the two disagree for as long as that descriptor lives —
  though both name the copy (§4.4.3).
- **A directory's link count is always 1.** The true count of
  subdirectories spans strata and cannot be maintained (§4.4.3).
- **Directory positions do not survive a reopen.** A `readdir` offset
  is an ordinal into a per-descriptor capture, carrying no provider
  cookie (§4.3.4).

Every one of the first four is the result of refusing to invent a
hidden record — a whiteout — that would make an entry in someone else's
directory unreachable without their knowledge. The alternative buys
POSIX fidelity at the cost of a stratum no longer meaning what its
owner wrote in it, which is the property §4.2.1 exists to protect.

---

# Appendix 4.A Constants

_Peios / Advanced Peios / PKM / stratafs_

> Every stratafs constant — filesystem identity, stratum flags, extended attributes, copy-up and staging markers — generated from the source.

Every value below is generated from the source by
`pkm/tools/gen-stratafs-constants.py`. Nothing here is transcribed by
hand, and the generator's `--check` mode fails if the two drift apart.

## 4.A.1 Filesystem identity

| Constant              | Value        | Meaning                                         |
|-----------------------|--------------|-------------------------------------------------|
| `STRATAFS_MAGIC`      | `0x53545241` | Superblock magic, reported by `statfs` (§4.2.2) |
| `STRATAFS_NAME`       | `"stratafs"` | The name the filesystem registers under         |
| `STRATAFS_MAX_STRATA` | `16`         | Longest stratum stack accepted (§4.2.1)         |

`STRATAFS_MAGIC` is an alias for `STRATAFS_SUPER_MAGIC`, which is
declared in the header stratafs shares with KACS so that the mount
policy class keyed on it cannot drift (§4.6.4).

## 4.A.2 Stratum flags

| Constant            | Value | Meaning                    |
|---------------------|-------|----------------------------|
| `STRATAFS_F_CREATE` | `0x1` | The `create` flag (§4.2.1) |
| `STRATAFS_F_RO`     | `0x2` | The `ro` flag              |
| `STRATAFS_F_AM`     | `0x4` | The `am` flag              |

## 4.A.3 Extended attributes

| Constant                 | Value                               | Meaning                                                  |
|--------------------------|-------------------------------------|----------------------------------------------------------|
| `STRATAFS_XATTR_PREFIX`  | `"system.stratafs."`                | Reserved namespace; never forwarded to a provider (§4.7) |
| `STRATAFS_XATTR_ORIGIN`  | `"system.stratafs.origin"`          | Synthetic, read-only, hidden from listings (§4.7)        |
| `STRATAFS_XATTR_STAGING` | `"security.peios.stratafs_staging"` | Copy-up staging marker; also reserved (§4.5.2)           |

`STRATAFS_XATTR_STAGING` is an alias for `STRATAFS_STAGING_XATTR`,
declared in the shared header. Note the name it resolves to lies
outside the reserved `system.stratafs.` namespace, yet receives the
same treatment (§4.7).

The canonical security-descriptor attribute is KACS's, not
stratafs's; stratafs only detects it in order to exclude it from
copy-up replication (§4.6.3).

## 4.A.4 Copy-up and staging

| Constant                        | Value                | Meaning                                     |
|---------------------------------|----------------------|---------------------------------------------|
| `STRATAFS_STAGE_MARKER_MAGIC`   | `0x53544731`         | Marker magic                                |
| `STRATAFS_STAGE_MARKER_VERSION` | `1`                  | Marker version                              |
| `STRATAFS_COPY_BUFFER_SIZE`     | `65536`              | Copy-up read/write chunk, in bytes (§4.5.2) |
| `STRATAFS_STAGE_PREFIX`         | `".stratafs-stage-"` | Staged-name prefix                          |
| `STRATAFS_RECOVERY_BATCH`       | `128`                | Names scanned per staging-recovery pass     |

### 4.A.4.1 The staging marker

`struct stratafs_stage_marker` is packed and 24 bytes, all fields
little-endian. It is the value of the staging attribute above.

| Offset | Size | Field          | Type     |
|--------|------|----------------|----------|
| 0      | 4    | `magic`        | `__le32` |
| 4      | 2    | `version`      | `__le16` |
| 6      | 2    | `size`         | `__le16` |
| 8      | 8    | `boot_cookie`  | `__le64` |
| 16     | 8    | `mount_cookie` | `__le64` |

## 4.A.5 Routing

The value `route_existing` returns (§4.5.1), as the Rust decision
core names it and as the C glue mirrors it. The discriminants match.

| C enumerator               | Value | Rust       |
|----------------------------|-------|------------|
| `STRATAFS_ROUTE_IN_PLACE`  | `0`   | `InPlace`  |
| `STRATAFS_ROUTE_COPY_UP`   | `1`   | `CopyUp`   |
| `STRATAFS_ROUTE_READ_ONLY` | `2`   | `ReadOnly` |

## 4.A.6 The decision core

`stratafs-core` holds the stack-wide flag rules, provider selection,
and routing. Its flag bits match the C ones above exactly.

| Constant          | Value |
|-------------------|-------|
| `MAX_STRATA`      | `16`  |
| `FLAG_CREATE`     | `0x1` |
| `FLAG_READ_ONLY`  | `0x2` |
| `FLAG_ABSENT_MAY` | `0x4` |
| `FLAG_MASK`       | `0x7` |

The crate distinguishes these configuration errors. The C boundary
collapses all of them to `EINVAL`, so the distinction is not
observable to a caller (§4.2.1).

| Error            | Discriminant |
|------------------|--------------|
| `Empty`          | `1`          |
| `TooMany`        | `2`          |
| `UnknownFlag`    | `3`          |
| `RepeatedCreate` | `4`          |
| `CreateReadOnly` | `5`          |

## 4.A.7 Build configuration

stratafs is built by `CONFIG_STRATAFS_FS`, a boolean option, so what it
builds is linked into `vmlinux` rather than loaded. It depends on
`CONFIG_SECURITY_PKM` and selects `FS_STACK`. Its sources are staged
into the kernel tree as `fs/stratafs`, separate from PKM's own
`security/pkm`. `CONFIG_STRATAFS_KUNIT_TEST` builds the in-kernel unit tests.

The translation units are:

- `super.o`
- `lookup.o`
- `inode.o`
- `file.o`
- `dir.o`
- `xattr.o`
- `copy_up.o`

---

# 5.1 Overview

_Peios / Advanced Peios / PKM / LCS_

> LCS is the kernel half of the Peios registry — where it sits, its semantic core, what is layered and what is not, and where the model diverges.

LCS — the Layered Configuration Subsystem — is the kernel half of the
Peios registry: a hierarchical, access-controlled configuration store
modelled on the Windows registry. It owns the namespace, the security
model, change observation, transactions, and the layer system that
gives the registry its name. It owns no storage at all.

Storage belongs to **sources**: userspace processes that hold registry
data and answer questions about it over the Registry Source Interface.
A source stores what it is told and returns everything it holds; it
never resolves layers, never filters by visibility, never sees the
identity of a caller, and never interprets a path beyond the parent and
child names it is given. Every decision about what a caller may see or
do is made in the kernel. loregd is the first source, and the one that
provides `Machine\` and `Users\` at boot, but nothing in LCS knows that:
hive routing is built entirely from what registers.

Userspace never talks to a source. Processes reach the registry through
three syscalls and eighteen ioctls, and the fd those syscalls return is
a capability — an open key carries the access mask it was granted, and
carries it wherever the fd goes.

## 5.1.1 Where it sits

LCS is a subsystem of PKM, peer to KACS and KMES, staged into the
kernel tree as `security/pkm/lcs` and built by `CONFIG_SECURITY_PKM` — a
boolean option, so it is linked into `vmlinux` rather than loaded. Its
three syscalls occupy 1100–1102 in the PKM range, added to the syscall
table by a patch against `arch/x86/entry/syscalls/syscall_64.tbl`.

It depends on KACS and KACS does not depend on it. Every access
decision LCS makes is a call into the KACS AccessCheck function against
a Security Descriptor the source returned; LCS defines no access
control mechanism of its own. Security Descriptor inheritance at key
creation is likewise KACS's computation, not LCS's. Audit events go to
KMES.

A substantial part of LCS is Rust. The crate `lcs-core` is staged
alongside PKM's other cores as `security/pkm/lcs/lcs_core` and holds
the parts where correctness is a matter of pure decision rather than
kernel plumbing: layer resolution, the RSI wire codec, the backup
stream serialiser, the transaction mutation log, watch dispatch, case
folding, and configuration validation. The C half owns fds, the char
device, memory, locking, and the syscall boundary.

## 5.1.2 The semantic core

Six rules generate the rest of the model. Every behaviour described in
this chapter is a consequence of one of them.

1. **Names are layered.** A key's presence at a path is per-layer.
   Different layers can name different keys at the same path, and
   removing a layer removes its names.
2. **Values are layered.** Every write is tagged with a layer; the
   effective value is the highest-precedence entry. Tombstones can
   actively mask lower layers.
3. **Key identity is not layered.** A key's GUID, Security Descriptor,
   volatile flag, symlink flag and last write time belong to the key
   object, not to any layer, and are never automatically reverted.
4. **Security is key-bound, not layer-bound.** Modifying a Security
   Descriptor is a permanent change to the key. Removing a layer does
   not revert it. Security policy is operational state, not
   configuration overlay.
5. **Handles are capabilities granted at open.** An open key fd carries
   an access mask computed once, by AccessCheck, at open time. Later
   operations test the mask, not the descriptor. Changing a descriptor
   does not affect an fd that already exists.
6. **Sources persist, the kernel decides meaning.** A source stores
   path entries, key records and value entries and returns all of them.
   Everything else is the kernel's.

The consequence that surprises people most often is the fourth. A
layer is a configuration overlay and reverts cleanly; an access control
change is not configuration and does not.

## 5.1.3 What is layered and what is not

| Property | Layered | Resolved by | Survives layer deletion |
|---|---|---|---|
| Path existence | Yes | Highest precedence, then highest sequence | No |
| Values | Yes | Highest precedence, then highest sequence | No |
| Value tombstones | Yes | Masking lower precedence | No |
| Blanket tombstones | Yes | Masking all lower-precedence values on the key | No |
| Key hiding | Yes | Masking lower precedence | No |
| Key GUID | No | Direct on the key object | Yes |
| Security Descriptor | No | Direct on the key object | Yes |
| Volatile flag | No | Direct on the key object | Yes |
| Symlink flag | No | Direct on the key object | Yes |
| Last write time | No | Direct on the key object | Yes |

A watch is bound to the key object rather than to either, and stays on
that object whatever happens to the name (§5.6.3).

## 5.1.4 registry.pol

One external format constrains the design. `registry.pol` is the binary
format Active Directory Group Policy uses to deliver configuration to
domain-joined machines, and the registry exists so that Peios can
consume it without loss. Everything `registry.pol` can express, the
registry can represent.

Three consequences run all the way through:

- The **full Windows value type set** is supported, including the three
  hardware-resource types that carry no Peios semantics at all. They
  behave exactly as `REG_BINARY`; they exist so a value copied from a
  Windows hive round-trips with its type tag intact (§5.2.6).
- **Paths are backslash-separated, case-preserving and
  case-insensitive.** This is not negotiable and it is the reason case
  folding appears in the kernel at all (§5.2.8).
- **Registry access rights occupy the Windows bit positions**, so a
  Security Descriptor containing registry ACEs is binary-compatible
  (§5.4.2).

Tombstones and blanket tombstones exist for the same reason: the
`**Del.ValueName` and `**DelVals` directives express *absence*, which a
purely additive overlay cannot (§5.2.7).

LCS does not parse `registry.pol`. Parsing is a userspace concern; LCS
provides the model that makes a faithful translation possible.

No other parity with Windows is claimed. The binary compatibility of a
Security Descriptor is a KACS guarantee, not an LCS one.

## 5.1.5 Where the model diverges from Windows

LCS is modelled on the Windows Configuration Manager, and departs from
it in seven places. All seven are decisions rather than gaps.

| | Windows | LCS |
|---|---|---|
| Backing store | Kernel-internal hive files | Userspace sources over the RSI, so a storage backend is not a kernel change |
| Hive routing | A fixed set of predefined hives | Any source may register any name at runtime |
| Layers | None; `registry.pol` is applied by flattening values | Precedence-ordered layers with tombstones, resolved at query time, so removal reverts rather than tattoos |
| Change observation | `RegNotifyChangeKeyValue`, single-shot | Persistent watches, closing the re-registration race |
| Key identity | Hive cell offsets | GUIDs, stable across storage reorganisation |
| Forward slash | Not accepted | Accepted on input, normalised to backslash |
| Case comparison | `RtlCompareUnicodeString` | Unicode Simple Case Folding, pinned to a version |

## 5.1.6 Windows features that are absent

Four have been evaluated and deliberately excluded.

**Key classes.** The `class` parameter of `RegCreateKeyEx` is
documented by Microsoft as reserved, and no consumer of it is known.

**`RegOverridePredefKey`.** Per-process key redirection, and specific
to COM. It would require unbounded per-process state in the kernel, and
private hives and private layers cover the uses that are legitimate.

**WoW64 redirection.** Splitting keys by pointer width. Peios has no
32-bit compatibility concern to split for.

**The `HKEY_CLASSES_ROOT` merged overlay.** A merge of `HKLM` and
`HKCU` `Software\Classes`, again COM-specific, with no Peios
equivalent.

## 5.1.7 This chapter

§5.2 covers the data model — hives, keys, path entries, values,
tombstones, names, and what happens to a key that loses its last name.
§5.3 covers layers: the model, the base layer, where layer metadata
lives and the circularity that implies, who may write into a layer, and
the resolution algorithm and the sequence counter that drives it. §5.4
covers security: the access flow, the rights, inheritance, and the audit
events LCS emits. §5.5 covers the syscall and ioctl interface and the
error model. §5.6 covers watches, §5.7 transactions, and §5.8 the source
model — registration, dispatch, validation, and the intricate business
of what a late response means. §5.9 covers backup and restore, §5.10 how
LCS configures itself out of the registry it is serving, and §5.A the
ABI.

Two contracts extracted from LCS are specified rather than described,
because a third party implements the other side of each: the Registry
Source Interface and the registry backup format. Both are chapters of
PSPK.

---

# 5.2.1 Hives and Routing

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> A hive is the first component of every path — the routing table that maps hive names to sources, and the two names the kernel knows itself.

A hive is a top-level namespace — the first component of every registry
path. LCS keeps a routing table mapping hive names to registered
sources, and that table is built entirely from what registers. There is
no static configuration.

| Field | Description |
|---|---|
| Name | The hive name, e.g. `Machine`, `Users`. Case-preserving, compared case-insensitively. |
| Root GUID | The GUID of the hive's root key, supplied by the source at registration. |
| Source | The source slot backing this hive. |
| Status | Active or Unavailable, tracking source connectivity. |

Status is a property of the source slot rather than of the individual
hive, which amounts to the same thing: a slot's hives are exactly one
source's, and they go Down together.

## 5.2.1.1 Route identity

A hive route is identified by the pair **(case-folded name, scope)**,
where the scope is either the global namespace or one private scope
GUID. That pair must be unique across every registered source; a source
claiming one another source already holds is rejected.

The same name may appear in different scopes, which is precisely how a
private hive shadows a global one without colliding with it.

A hive is backed by exactly one source. A source may back many.

## 5.2.1.2 Routing a path

LCS takes the first component of a path — everything before the first
separator — and looks it up. Registered and active routes to the
backing source. Not registered is `ENOENT`. Registered but Down is
`EIO`.

Before any source registers, the table is empty and every path yields
`ENOENT` (§5.10.1).

## 5.2.1.3 `CurrentUser` is not a hive

`CurrentUser\` is a kernel-level alias. When a caller-supplied absolute
path starts with it, LCS reads the user SID from the calling thread's
effective token and rewrites the path to `Users\<SID>\...` before
routing. The rewritten path is re-checked against the total-length
limit, since a textual SID is longer than the alias it replaced.

Three constraints keep this safe.

It applies **only to the first component** of a path, and only to a
caller-supplied absolute one. It does **not** apply to symlink targets:
a target beginning with `CurrentUser\` is followed literally, routes as
a hive of that name, and finds nothing. Without that rule, a symlink
containing `CurrentUser\` would redirect a privileged service into the
service's own user hive — a confused deputy.

And `CurrentUser` cannot be registered as a hive name. LCS rejects the
registration with `EINVAL`, so the alias can never collide with a real
hive.

Symlink targets *are* subject to ordinary hive routing, private hives
included. A sandboxed process resolving a symlink sees the same
registry through it that it sees directly, which is the point of
sandboxing it.

## 5.2.1.4 Two names the kernel knows

LCS is source-agnostic about routing but not entirely ignorant of
names. `Users` is compiled in as the target of `CurrentUser\`
rewriting. `Machine` is matched, case-insensitively and only for a
global hive, to decide when to run the bootstrap refresh that reads
LCS's own configuration (§5.10.2).

Neither is a routing decision — an unregistered `Machine` routes
nowhere like any other name — but the two names do exist in the kernel.

In practice loregd registers `Machine` and `Users` at boot. Other
sources may register other hives at any time.

## 5.2.1.5 Names

Hive names follow the same rules as key name components: no backslash,
no forward slash, no null byte, valid UTF-8, non-empty, and no longer
than `MaxPathComponentLength`. They are case-preserving and compared
case-insensitively (§5.2.8).

---

# 5.2.2 Private Hives

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> Hives invisible in the global namespace, reachable only through a scope GUID on the calling token — routing, lifecycle and registration rules.

A private hive is registered with the `RSI_HIVE_PRIVATE` flag and a
scope GUID. It is invisible in the global namespace and reachable only
by a thread whose token carries that scope GUID.

## 5.2.2.1 Routing

Private hives are checked **before** global ones. For each scope GUID
on the calling thread's token, in the order the token carries them, LCS
looks for a private hive with the path's name and that scope. The first
match wins. Only if none matches is the global table consulted.

A private hive can therefore shadow a global one, and no name is exempt
— a thread with a private `Machine\` sees the private one. That is what
makes complete registry isolation possible for a container or a
sandbox without giving it a differently-named hive to notice.

`MaxScopeGUIDsPerToken`, default 8, bounds the per-syscall iteration
cost. Duplicate scope GUIDs on a token are rejected.

## 5.2.2.2 Scope GUIDs on a token

Scope GUIDs reach a thread through the KACS token's LCS credential
extension: a versioned block in the token specification carrying the
thread's scope GUIDs and its private layer names (§5.3.5). It parses at
a fixed offset, rejects nil and duplicate GUIDs, and caps the count at
256 — a hard KACS limit, above the configurable LCS one. The
credentials propagate across fork, duplication and impersonation like
the rest of the token.

Attaching them is gated by `SeCreateTokenPrivilege`, because scope
GUIDs can only enter a token when the token is created. That is a
blanket privilege rather than a per-scope authorisation: a caller that
can create tokens at all can create one claiming any scope. Isolation
between private hives therefore rests on who may create tokens, not on
who owns a scope.

## 5.2.2.3 Scope lifecycle

A scope GUID is an opaque 128-bit value with no lifecycle. LCS neither
creates nor tracks one; it only compares. A scope exists as long as
some private hive or some token references it, and when the last
reference goes it is simply a number nobody uses.

Nothing in the kernel generates scope GUIDs. Key GUIDs and token GUIDs
come from the kernel's UUIDv4 generator; scope GUIDs are supplied by
userspace, and choosing them unpredictably is userspace's
responsibility.

## 5.2.2.4 Registration rules

Registration enforces more than the route-identity uniqueness of
§5.2.1:

- A hive's root GUID may not be nil, and the root GUIDs within one
  registration request must be distinct.
- A hive without the `RSI_HIVE_PRIVATE` flag may not carry a non-nil
  scope GUID, and a hive **with** it may not carry a nil one. No token
  can carry a nil scope GUID, so such a hive would be registrable and
  then permanently unroutable — holding its name against other sources
  in that scope while every lookup returned `ENOENT`.
- Unknown flag bits are rejected.
- Opening `/dev/pkm_registry` at all requires an **enabled**
  `SeTcbPrivilege`, checked in the device's `open()` handler
  (§5.8.2).

---

# 5.2.3 Keys

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> A key is a container of subkeys and values — its identity, its properties, volatile keys, and the rules for naming one.

A key is a node in the hierarchy: a container holding subkeys and
values. The filesystem analogy is an inode. A key carries identity and
properties; naming lives in path entries (§5.2.5).

| Field | Mutable | Layered | Description |
|---|---|---|---|
| GUID | No | No | Identity, assigned by LCS at creation, persisted by the source. |
| Name | No | No | The key's own name component. Informational; the authoritative name is in the path entry. |
| Parent GUID | No | No | The parent key. Nil for a hive root. |
| Security Descriptor | Yes | No | Computed from the parent at creation; changed at runtime with `WRITE_DAC` / `WRITE_OWNER`. |
| Last write time | Yes | No | Updated when a value is written or deleted or the descriptor changes. |
| Volatile | No | No | The source stores this key in non-persistent storage only. |
| Symlink | No | No | This key is a symbolic link (§5.2.4). |

**No key property is layer-qualified.** Properties belong to the object.
Changing a key's structural type through a layer is not a matter of
setting a flag; the layer hides the original key and creates a new one
at the same path, which is the ordinary overlay pattern.

## 5.2.3.1 Identity

A key's identity is its GUID, not its path. Two keys occupying the same
path at different times are different objects with different GUIDs. A
path is a name that maps to an identity; the identity outlives the
name.

GUIDs are assigned by LCS — never by a source — and pushed to the
source at creation, where they become the primary key of its storage.
Once a path has been resolved to a GUID at open time, every subsequent
RSI operation uses the GUID directly.

The generator is the kernel's UUIDv4: random bytes from the kernel
CSPRNG with the RFC 4122 version and variant bits set. Freshness is the
collision resistance of UUIDv4 plus a check against the keys LCS
currently tracks, with a bounded retry. There is **no persistent
retired-GUID catalogue**, and no code anywhere that would maintain one.
A GUID dropped by orphan cleanup is not remembered.

If a source answers `RSI_CREATE_KEY` for a freshly generated GUID with
`RSI_ALREADY_EXISTS`, that is not a race to retry — the kernel believes
the GUID is unused and the source disagrees. LCS treats it as source
inconsistency and fails closed with `EIO`. It is never retried as an
open and `EEXIST` never reaches userspace from `reg_create_key`.

A GUID appears at exactly one canonical location. LCS validates that
and rejects a source that reports otherwise.

## 5.2.3.2 Volatile keys

Volatile is a flag LCS carries and forwards. Storing a volatile key in
non-persistent storage is the source's obligation; nothing in the
kernel enforces it.

What the kernel does enforce is the containment rule: **a non-volatile
key may not be created under a volatile parent** (`EINVAL`). The
converse is allowed — a volatile key under a persistent parent is
ordinary.

## 5.2.3.3 Naming

Three bytes are forbidden in a key name component: backslash and
forward slash, both of which are separators, and the null byte. Every
other valid UTF-8 sequence is permitted, spaces and arbitrary Unicode
included.

Empty components are forbidden, which rules out a leading separator and
consecutive separators (`Machine\\System`). A trailing separator
(`Machine\System\`) is likewise invalid.

Length limits are byte counts: `MaxPathComponentLength` per component,
`MaxTotalPathLength` for the whole path, `MaxKeyDepth` for nesting.

Forward slash normalisation is achieved by treating `/` as a separator
wherever a separator is recognised, rather than by rewriting the string
— so a materialised component never contains either separator, and the
canonical form is per-component rather than a canonicalised string.

---

# 5.2.4 Symlinks

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> The two load-bearing mechanisms behind a symlink key — the structural flag and the target value — plus resolution, depth and opening the link itself.

A symlink key uses two mechanisms, and both are load-bearing.

The **symlink flag** on the key record marks the key's structural type.
It is set at creation by `REG_OPTION_CREATE_LINK` and is immutable
afterwards — `RSI_WRITE_KEY` can update only the descriptor and the
last write time, so there is no operation that could change it.

The **default value, of type `REG_LINK`,** supplies the target path. It
is an ordinary layered value, which means a higher-precedence layer can
redirect a symlink by writing a different `REG_LINK` default value, and
removing that layer restores the original target.

The flag marks identity; the value provides the target.

## 5.2.4.1 Resolution

LCS follows symlinks during path resolution and the fd it returns
refers to the resolved target — its GUID, its position in the tree, its
ancestor chain — not to the link.

The target is resolved by issuing a separate `RSI_QUERY_VALUES` for the
key's default value (the empty name) and applying ordinary layer
resolution to the result, so the target participates in the layer
system exactly as any other value does.

If the effective default value is missing, or is not of type
`REG_LINK`, resolution fails with `EINVAL`. LCS does **not** validate
the type at write time: a layer that writes a `REG_SZ` default value
over a symlink's target breaks resolution at the next open, and
removing that layer fixes it. The offending value stays in the
registry; a failed resolution writes nothing.

## 5.2.4.2 The target path

The `REG_LINK` payload is a length-delimited UTF-8 registry path. No
trailing null is required or permitted — the length delimits it, and a
null byte inside that length is rejected like any other. Forward
slashes are handled as separators as everywhere else.

The target is validated with exactly the same rules as a syscall path:
UTF-8, no null bytes, per-component and total length, no empty
components, no trailing separator, maximum depth. The only difference
is that a syscall path arrives null-terminated and has its terminator
stripped first.

A target is always interpreted as absolute — its first component is
routed as a hive name. There is no check that rejects a relative-looking
target as malformed. A target of `Sub\Key` is not an error; it is a
request for a hive named `Sub`, and it yields `ENOENT` unless such a
hive happens to be registered, in which case it resolves there.

`CurrentUser\` rewriting is not applied (§5.2.1), so a target beginning
with `CurrentUser\` routes as a hive of that name and cannot be
registered, and therefore always fails. Ordinary hive routing does
apply, private hives included, for the resolving thread.

## 5.2.4.3 Depth

Symlink resolution is bounded by `SymlinkDepthLimit`, default 16,
configurable from 1 to 64. Exceeding it is `ELOOP`.

Two paths in the walk use the compiled-in default rather than the
configured value, so a `SymlinkDepthLimit` other than 16 is not honoured
everywhere.

## 5.2.4.4 Opening the link itself

`REG_OPEN_LINK` on `reg_open_key` opens the symlink key rather than
following it, which is how a symlink is managed at all — deleted,
retargeted, inspected.

It applies to the **final path component only**. A symlink encountered
part-way along a path is followed whether or not the flag is set. The
access check follows the same rule: with `REG_OPEN_LINK` the check is
against the link, otherwise against the target.

## 5.2.4.5 Creation

Creating a symlink needs all of:

- `KEY_CREATE_SUB_KEY` on the parent, as for any key;
- `KEY_CREATE_LINK` on the parent;
- either an enabled `SeTcbPrivilege` or membership of Administrators.

The last is a genuine disjunction — either satisfies it — and the
privilege branch marks the privilege used. Failing it is `EPERM`.

---

# 5.2.5 Path Entries

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> Key existence and naming are layer-qualified while key identity is not — the per-layer path entry that separates them, and hiding.

Key existence and naming are layer-qualified; key identity is not. The
source stores a **path entry** per layer, which separates the two. The
overlay-filesystem analogy is exact: directory entries are per-layer,
inodes are shared.

| Field | Description |
|---|---|
| Parent GUID | The parent key. |
| Child name | The key's name under that parent — one component, not a path. |
| Layer | The layer this entry belongs to. |
| Target | The GUID of the key at this path in this layer, or HIDDEN. |
| Sequence | A monotonic number assigned by LCS at creation, used for tiebreaking within a precedence tier. |

On the wire a HIDDEN entry is a target type of 1 with an all-zero GUID
(§5.A). A source returning a non-zero GUID on a HIDDEN entry is
returning malformed data.

## 5.2.5.1 Creating a key

Creating a key in a layer always assigns a fresh GUID and produces two
records: a path entry `(parent, name, layer) → GUID` with a new
sequence number, and a key record carrying that GUID.

LCS sends `RSI_CREATE_ENTRY` first and `RSI_CREATE_KEY` second. That
order matters for the race: if another caller got there first, the
entry creation returns `RSI_ALREADY_EXISTS` and LCS retries the whole
thing as an open, reporting `REG_OPENED_EXISTING`. Failure on the
*second* call, for a GUID LCS just minted, is not a race and fails
closed (§5.2.3).

If a different layer already has a key at that path, the new layer gets
its own distinct GUID. Each layer has its own key object, and
resolution decides which is visible.

**No operation creates a path entry pointing at an existing GUID.**
`reg_create_key` always mints a new one. The namespace is a tree, never
a graph.

## 5.2.5.2 No GUID sharing across layers

The path table's shape would technically permit two entries referencing
one GUID, which would be a hard link. No API exposes it. Every key has
exactly one canonical parent and name, and LCS validates that a source
is not reporting otherwise.

Aliasing has an explicit, visible mechanism, and it is symlinks. Hard
links would make parent-GUID semantics, descriptor inheritance, subtree
enumeration and watch dispatch all ambiguous at once.

## 5.2.5.3 Hiding

A layer can create a HIDDEN entry at a path, making the key invisible
regardless of what lower-precedence layers say. It is the path-level
equivalent of a value tombstone, and when the hiding layer is removed
the lower key reappears.

A HIDDEN entry gets a sequence number like any other entry, so a
conflict between a GUID entry in one layer and a HIDDEN entry in
another at the same precedence resolves deterministically: the higher
sequence number wins.

## 5.2.5.4 Hide and replace

A single layer can hide an existing key and put a new one at the same
path, and no special mechanism is needed for it. The layer creates its
own path entry pointing at its own new GUID; lower-precedence entries
for the same `(parent, name)` are masked because this layer's entry
wins on precedence. Removing the layer removes both the new key and its
masking effect, and the lower key comes back.

This falls out of per-layer path entries and precedence ordering
without a line of code that knows about it.

## 5.2.5.5 Hive roots

A hive root has no parent GUID and no child name, so there is no
`(parent, name, layer)` tuple to remove or mask. Deleting or hiding a
hive root fd is `EINVAL`, rejected before source dispatch, before
transaction enlistment, before sequence allocation and before any watch
event is generated.

---

# 5.2.6 Values

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> A named, typed datum inside a key — one name with many layer entries, the type set, REG_TOMBSTONE, naming and size.

A value is a named, typed datum inside a key. Values hold the
configuration data; keys hold values.

| Field | Description |
|---|---|
| Key GUID | The key this value belongs to. |
| Name | The value's name; the empty string is the default value. Case-preserving, compared case-insensitively. |
| Type | A registry value type. |
| Data | An opaque byte array, up to `MaxValueSize` (default 1 MB). |
| Layer | The layer this entry belongs to. Every write is tagged. |
| Sequence | Assigned by LCS at write time, for tiebreaking within a precedence tier. |

A key holds many values with distinct names, and at most one unnamed
one.

## 5.2.6.1 One name, many entries

A single `(key GUID, value name)` pair can have several entries in the
source — one per layer that has written to it. The source stores them
all and returns them all; LCS resolves the effective value at read
time (§5.3.6).

That is the mechanism that makes layer deletion revert configuration
automatically. Delete the layer, its entries go, and the
next-highest-precedence entry becomes effective. Nothing has to be
recomputed or rewritten.

## 5.2.6.2 Types

The full Windows type set is supported, for `registry.pol` fidelity:
`REG_NONE`, `REG_SZ`, `REG_EXPAND_SZ`, `REG_BINARY`, `REG_DWORD`,
`REG_DWORD_BIG_ENDIAN`, `REG_LINK`, `REG_MULTI_SZ`,
`REG_RESOURCE_LIST`, `REG_FULL_RESOURCE_DESCRIPTOR`,
`REG_RESOURCE_REQUIREMENTS_LIST` and `REG_QWORD`, numbered 0 to 11.
Values are in §5.A.

LCS stores the type tag and returns it on read but does not interpret
the data. The single exception is `REG_LINK` read as a symlink key's
default value, which triggers target resolution (§5.2.4).

The three hardware-resource types, 8 to 10, describe device resource
assignments in the Windows `HKLM\HARDWARE` hive, which the Windows
kernel rebuilds at every boot. Peios has no equivalent: hardware
enumeration belongs to the Linux device model, sysfs and `/proc/iomem`,
not to the registry. LCS accepts these tags only so a value carrying
one round-trips without loss. They have no semantics, LCS never
produces one, and for every operation they behave as `REG_BINARY`.
Rejecting a faithfully-copied value would break the fidelity guarantee
that is the reason the registry exists.

Type tags are validated at every write boundary, before sequence
allocation, transaction enlistment or source dispatch. An unknown code
is `EINVAL`.

## 5.2.6.3 `REG_TOMBSTONE`

One further type, `REG_TOMBSTONE` (`0xFFFF`), is internal. It is never
returned to a caller reading a value — a caller whose effective entry
is a tombstone gets `ENOENT`, which is exactly what a tombstone means.

There is no separate tombstone flag in the `REG_IOC_SET_VALUE`
argument. Writing type `REG_TOMBSTONE` **is** the explicit tombstone
operation, and it must carry zero-length data; non-empty tombstone data
is `EINVAL`, again before sequence allocation, enlistment or dispatch.

## 5.2.6.4 Naming

Value names use the same case rules as key names: Unicode Simple Case
Folding, case-preserving, case-insensitive.

They differ in one respect. **Backslash and forward slash are permitted
in a value name.** Value names are not hierarchical, so a separator has
no meaning in one. Only the null byte is forbidden, and invalid UTF-8 is
rejected. The empty string is reserved for the default value.

That difference is why watch event path components are length-prefixed
rather than joined by a separator (§5.6.2) — a value name can contain
the separator.

## 5.2.6.5 Size

`MaxValueSize` defaults to 1 MB and is configurable from 4 KB to 64 MB
(§5.10.3). Value data is opaque bytes and is not subject to the UTF-8
validation that applies to every string in the interface.

---

# 5.2.7 Tombstones

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> An overlay that can only add is not enough — value and blanket tombstones, how they compete, and what a watcher sees.

An overlay that can only add is not enough. `registry.pol` expresses
*absence* — `**Del.ValueName` deletes a specific value,
`**DelVals` deletes every value in a key before applying new ones — and
a higher-precedence layer that can only override cannot say "this value
must not be configured". Tombstones are how absence is expressed.

Both kinds are per-layer, and both vanish with their layer, restoring
whatever they were masking.

## 5.2.7.1 Value tombstones

A value tombstone is a layer entry that says the value does not exist
in this layer and lower-precedence layers are masked. Resolution treats
a winning tombstone as "not found" without falling through.

In the source's storage it is an entry of type `REG_TOMBSTONE` with no
data. A caller who wins with one gets `ENOENT`.

Removing the tombstone's layer makes the lower-precedence value
effective again, which is the whole point.

## 5.2.7.2 Blanket tombstones

A blanket tombstone is a per-layer marker on a **key** that masks every
value from lower-precedence layers, whatever its name. Where a value
tombstone names one value, a blanket names none and covers all —
including values whose names were not known when the blanket was
written, which is exactly what `**DelVals` requires.

It is stored as a flag on the `(key GUID, layer)` relationship and
occupies no per-name entry. It has its own sequence number.

### 5.2.7.2.1 How it competes

A blanket does not short-circuit resolution. It enters the candidate
pool as a tombstone candidate **for every value name**, at its own
`(precedence, sequence)`, and the ordinary rule picks the winner
(§5.3.6).

So a per-value entry that beats the blanket on the tuple overrides it,
and one that loses is masked. A layer can write a blanket *and* write
specific values in the same layer: the specific values are visible
because they were written afterwards and carry higher sequence numbers,
and everything else from below is masked. That is `**DelVals` followed
by new writes, expressed without a special case.

An exact tie — same precedence *and* same sequence — between a blanket
and a per-value entry is not resolved in the blanket's favour or
anyone's. It is malformed source data and the operation fails with
`EIO` (§5.3.7).

### 5.2.7.2.2 Enumeration

Enumerating a key with a blanket applies the same per-name rule to each
name, so what a caller sees is the set of names whose winning candidate
is not the blanket. It is not simply "the blanket's layer and above": a
different layer at the *same* precedence with a higher sequence number
surfaces, and one with a lower sequence number does not.

### 5.2.7.2.3 Removal

Removing a blanket, or the layer holding it, unmasks everything it was
hiding.

## 5.2.7.3 What a watcher sees

Nothing about tombstones. Writing a blanket produces one
`VALUE_DELETED` per name it newly masks; removing one produces one
`VALUE_SET` per name that became visible. A watcher sees per-value
effective state and never has to know the mechanism (§5.6.1).

---

# 5.2.8 Names and Case

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> Every string in the LCS interface is UTF-8 — length as byte count, separators, and how case folding works.

Every string in the LCS interface — key names, value names, hive names,
layer names, paths — is UTF-8. Invalid UTF-8 is rejected with `EINVAL`
before parsing, routing, folding, layer resolution or source dispatch.
Null bytes are rejected in all of them.

The one thing that is not a string is value data, which is opaque
bytes.

## 5.2.8.1 Lengths are byte counts

Every configured length limit is measured in UTF-8 bytes, not Unicode
scalar values and not display characters. `MaxPathComponentLength`
(default 255) bounds one component or one value or layer name;
`MaxTotalPathLength` (default 16383) bounds a whole path; `MaxKeyDepth`
(default 512) bounds nesting.

A syscall path arrives as a null-terminated C string, is copied under a
hard bound, and has its terminator stripped before anything is
measured — so the terminator is not part of the length. Ioctl and RSI
strings are length-delimited and need no terminator; a terminator byte
included in the length is a null byte and is therefore invalid.

## 5.2.8.2 Separators

Backslash is canonical. Forward slash is accepted on input and treated
as a separator wherever a separator is recognised, so a component can
never contain either. There is no string-rewriting step: normalisation
is a property of how paths are split rather than a transformation
applied to them.

## 5.2.8.3 Case folding

Comparison is case-insensitive and storage is case-preserving. The
algorithm is **Unicode Simple Case Folding** — the `C` and `S` status
entries of `CaseFolding.txt`, with the full (`F`) and Turkic (`T`)
entries excluded. It is a fixed one-to-one codepoint mapping with no
locale input, applied after decoding from UTF-8, never to raw bytes.

The Unicode version is **pinned at 16.0**. The table is generated by
`pkm/tools/lcs/generate_casefold_table.py` and checked in with the
digest of the source data, so adopting a newer Unicode version means
regenerating the table deliberately. It is not something that happens
by updating a dependency.

This gives practical compatibility with Windows'
`RtlCompareUnicodeString` without claiming byte-identical behaviour in
every edge case.

**Unicode normalisation is not performed.** NFC and NFD forms of the
same visual character are different names, matching Windows.

Case folding is what "identity" means throughout: a layer's identity is
its folded name, a hive route's identity includes its folded name, and
a duplicate is a folded-equal duplicate. `RoleA` and `rolea` are one
layer, not two.

Two comparisons in the kernel are ASCII-only rather than folded: the
check for whether a layer name is `base` on two of its call sites, and
KACS's duplicate check when parsing private layer names into a token.
For the literal string `base` the two agree; for arbitrary names they
do not.

---

# 5.2.9 Deletion and Orphans

_Peios / Advanced Peios / PKM / LCS / The Data Model_

> Deletion works on two levels because naming and identity are separate — layer deletion, orphaned keys, and what happens to their watches.

Deletion operates on two levels, because naming and identity are
separate things.

## 5.2.9.1 Deleting a key

`REG_IOC_DELETE_KEY` removes **one layer's path entry**. The key's data
— its GUID, descriptor and values — is untouched; only a name is
removed. LCS derives the parent GUID from the fd's ancestor chain and
the child name from the last component of its resolved path, and sends
`RSI_DELETE_ENTRY`.

If path entries remain in other layers, the key is still visible
through them. If none remain anywhere, the key is orphaned.

A key with **visible children** cannot be deleted: `ENOTEMPTY`.
Visibility is evaluated globally, across all enabled layers, and
deliberately ignores the caller's private layer set — so whether a
deletion succeeds does not depend on who is asking. Recursive deletion
is a client-side tree walk, not a kernel primitive.

Deleting a key does not delete its values. Values belong to the GUID,
not to the path entry, and go when the GUID goes.

Hive roots cannot be deleted or hidden (§5.2.5).

## 5.2.9.2 Layer deletion

Deleting a layer removes all of its path entries, value entries and
blanket tombstones, across every source. LCS broadcasts
`RSI_DELETE_LAYER` and each source returns the GUIDs that lost their
last path entry as a result.

Effects on live state follow from the model with no special cases: keys
named only by that layer become orphaned; where the layer held the
winning value entry, the next layer's value becomes effective; blanket
tombstones it held are removed and unmask what they were hiding; and
Security Descriptors are unchanged, because they were never layered.

Watchers are notified by whatever recovery mechanism applies —
per-key events for the orphaned keys, and a source-wide `OVERFLOW` for
the rest (§5.6.3).

Before sending `RSI_DELETE_LAYER`, LCS aborts every bound transaction
whose mutation log touched that layer. Otherwise a transaction could
commit writes into a layer that no longer exists. Those transactions
return `EINVAL` on their next operation or commit attempt.

Layer deletion is what role uninstallation and Group Policy removal
are.

## 5.2.9.3 Orphaned keys

An orphaned key is a GUID with no path entry in any layer. It follows
the Linux unlink model: alive but unnamed.

Existing fds keep working. Operations that address the key by GUID
proceed normally:

- querying, setting and deleting values;
- setting and removing blanket tombstones;
- querying and setting the Security Descriptor;
- querying key metadata;
- flushing the key's hive;
- closing the fd.

Namespace operations return `ENOENT`:

- creating a child key under it;
- opening or creating anything relative to it;
- deleting its path entry;
- hiding it;
- backing it up.

The reason is that an orphaned key is no longer a reachable subtree
root. Allowing new names beneath an unnamed key would build a subgraph
nothing can reach.

## 5.2.9.4 Watches on an orphaned key

A watch armed before the key was orphaned stays armed, and the
transition delivers `KEY_DELETED`. After that the watch may still
observe GUID-local changes made through the surviving fds, though the
subtree is no longer expanded through the orphaned key.

Arming a *new* watch on an already-orphaned key is `ENOENT`. Re-arming
one that is already armed is allowed.

## 5.2.9.5 Dropping the GUID

When the last fd to an orphaned key closes and the source is Active,
LCS sends `RSI_DROP_KEY`, which purges the key record, every value
entry across every layer, and any remaining blanket tombstones. It is
dispatched **before** the in-kernel key state is released.

The request is asynchronous: nothing waits for the answer, and a valid
response is processed as an ordinary response rather than as a late
one (§5.8.5). That distinction is load-bearing — `RSI_DROP_KEY` is a
mutating operation, and without it the arrival of a perfectly normal
answer to a caller-less request would look like an unaccounted
mutation and tear the source down.

If the source is Down, LCS releases its in-kernel state and does **not**
queue a deferred drop. There is no deferred-drop queue. Recovering the
key record then falls to the source's own startup obligation to purge
records with no path entries before it becomes Active (§5.8.2).

`close()` never reports orphan cleanup failure to userspace, and never
can: it returns 0 unconditionally.

## 5.2.9.6 A new key at the same path

A layer can create a new key where another layer's key already exists.
Each layer has its own path entry pointing at its own GUID, and
resolution decides which is visible.

Fds referencing the other GUID are completely isolated from it:
different identity, different data, different value entries. Nothing
observable connects two keys that merely share a name.

---

# 5.3.1 The Layer Model

_Peios / Advanced Peios / PKM / LCS / Layers_

> A named collection of registry writes managed as a unit — precedence tiers, caps, and why layers are global while entries are per-source.

A layer is a named collection of registry writes that can be managed as
a unit. Layers have precedence, and the highest-precedence entry wins.
They are how role installation, Group Policy and configuration revert
all work, and they are the reason removing a role does not leave its
settings behind.

| Field | Mutable | Description |
|---|---|---|
| Name | No | The layer's identity — not a GUID, not an integer. Case-preserving, compared with Unicode Simple Case Folding. Bounded by `MaxPathComponentLength`. |
| Precedence | Yes | Higher wins. Default 0. |
| Enabled | Yes | A disabled layer is invisible during resolution unless it is attached to the resolving thread's credentials (§5.3.5). Default true. |
| Owner | — | The SID of the principal that created the layer. Informational only. |

The name being the identity is deliberate. Layer names are
code-generated and meaningful by construction —
`role-jellyfin`, `gpo-security-baseline` — so there is nothing an
opaque identifier would add.

`Owner` is never used for an access check; authorisation is the
descriptor on the layer's metadata key (§5.3.4). It is also not quite
immutable as a field: a refresh re-reads the `Owner` value and
re-selects it, so rewriting the value does change what is cached.
Because nothing consults it, that has no effect on anything.

## 5.3.1.1 Precedence tiers

The base layer and role layers all sit at precedence 0. Within one
tier, the most recent write wins — the highest sequence number. Group
Policy layers sit above 0 and override both.

Establishing or raising a layer's precedence above 0 requires
`SeTcbPrivilege` (§5.3.4). That is what keeps the tier boundary
meaningful.

## 5.3.1.2 Caps

`MaxTotalLayers`, default 1024, bounds the in-memory layer table.
Creating a layer when it is full returns `ENOSPC`.

The table itself is a fixed array sized at compile time for 1023
dynamic layers plus the base layer. `MaxTotalLayers` is configurable up
to 65536, and a value above 1024 validates and publishes, but the table
still runs out at 1023 dynamic entries. Values below 1024 bind
correctly.

`MaxLayersPerValue`, default 128, bounds how many layers may write to
the same `(key GUID, value name)` pair. It is a guard against
amplification — every read of that value has to resolve every entry —
not an access control boundary.

It is enforced at `REG_IOC_SET_VALUE` time, before the source is
contacted, by querying the source for the current entry count. A write
that replaces an existing entry in the *same* layer does not increase
the count and is not checked. Exceeding the cap is `ENOSPC`.

The check is deliberately **best-effort admission control**, not a
storage invariant. It queries and then dispatches without holding
anything, so concurrent writers can both observe room and both proceed.
Sources are not required to enforce it atomically. Once LCS observes a
count at or above the cap, further new-layer writes are refused.

Blanket tombstones and value deletions are not subject to it.

## 5.3.1.3 Layers are global; entries are per-source

There is one authoritative layer table, held by the kernel. Each source
stores layer *entries* — path entries and value writes tagged with
layer name strings — for its own hives. Layer *metadata* is global and
lives in one place.

A source never needs the layer table. It stores what it is told, tagged
with whatever name it is given, and returns everything on request. A
source that has never seen a particular layer name simply stores
entries carrying it. Resolution — precedence, enabled state, tombstone
evaluation — happens entirely in the kernel, and the layer snapshot is
passed into each operation rather than pushed to sources. There is no
RSI operation that hands a source the layer list.

---

# 5.3.2 The Base Layer

_Peios / Advanced Peios / PKM / LCS / Layers_

> The kernel-reserved layer that exists unconditionally before any source registers — its persisted metadata and the two ways it is matched.

The base layer, named `base`, is a kernel-reserved implicit layer. It
exists unconditionally, before any source registers and whether or not
any metadata has ever been persisted for it.

It is a static constant in the kernel: precedence 0, enabled. It is
never stored in the dynamic layer table, is always emitted first in
every layer snapshot, and is handed out even when the dynamic table is
empty. A source that registers with a completely empty database is
therefore immediately usable, because the one layer that writes need is
not in the database.

Four things cannot happen to it:

- it cannot be deleted;
- it cannot be disabled;
- its precedence cannot be changed;
- a layer table row for it cannot be published at all.

Each of those is enforced in more than one place. Deletion is refused by
the layer table, by the resolution core, by the `RSI_DELETE_LAYER`
dispatch path, and by the transaction layer-abort path. Publication of a
`base` row is rejected outright, and the refresh path short-circuits for
`base` **before** it would read `Precedence` or `Enabled`, so persisted
values for those are never even consulted.

## 5.3.2.1 Persisted metadata

`Machine\System\Registry\Layers\base\` may exist, and it usually does,
but it decorates the base layer rather than defining it. What LCS takes
from it is the metadata key's GUID and its cached Security Descriptor —
which is to say, who may write into the base layer (§5.3.4). Its
`Precedence` and `Enabled` values are ignored.

The internal self-watch also ignores a `SUBKEY_DELETED` for `base`: if a
higher-precedence HIDDEN entry masks the base layer's metadata key, that
is not a layer deletion and is not processed as one. The base layer's
existence is hardcoded and layer mechanics cannot reach it.

## 5.3.2.2 The default target

A write that names no layer targets the base layer. That is the default
for manual administration and for system initialisation.

Before the base layer's metadata key exists — first boot, before seed
restore — LCS uses a compiled-in default descriptor granting SYSTEM and
Administrators `KEY_ALL_ACCESS`, so writes into the base layer are
possible from the very beginning. The compiled-in default is replaced by
the real descriptor as soon as seed restore creates the key.

## 5.3.2.3 `base` is matched two ways

The check for whether a name is the base layer is implemented twice in
the kernel: once using Unicode Simple Case Folding like every other
name comparison, and once using ASCII case-insensitive comparison, on
two of its call sites. For the literal string `base` the two agree.

---

# 5.3.3 Layer Metadata

_Peios / Advanced Peios / PKM / LCS / Layers_

> Layer metadata lives in the registry it configures — the circularity that creates, why publication is atomic, and when the refresh runs.

Layer metadata lives in the registry, under
`Machine\System\Registry\Layers\<LayerName>\`. Each layer's key holds
three values:

| Value | Type | Default if missing |
|---|---|---|
| `Precedence` | `REG_DWORD` | 0 |
| `Enabled` | `REG_DWORD`, 0 or 1 | true |
| `Owner` | `REG_BINARY`, a SID | the creating token's SID for a new layer |

A value of the wrong type, or a `REG_DWORD` that is not exactly four
bytes, or an `Enabled` greater than 1, is malformed metadata and is
rejected rather than coerced.

`Owner` selection has a fallback chain: the metadata value; failing
that, the creator's SID for a newly created layer; failing that, the
previous known-good owner; failing that, the owner SID from the
metadata key's own descriptor. If none of those is available the layer
cannot be published. Every one of these is informational and none grants
access.

There is no "create layer" or "delete layer" syscall. Creating a key
under `Layers\` creates a layer; deleting that key deletes it.
Creation should be done inside a transaction so that all three values
are present when the refresh runs.

## 5.3.3.1 Circularity

Layer metadata is stored in the registry, which is itself layered.
That is circular, and it is safe, because resolution never re-enters
itself.

LCS always resolves using its **currently published** layer table —
including when resolving layer metadata values. When a write to the
metadata subtree commits, the refresh reads the affected metadata using
the current table and then publishes an updated one. The table is never
re-resolved mid-operation; each operation takes one snapshot and uses it
throughout.

So a high-precedence layer can override another layer's precedence, and
that is useful and intended. It simply takes effect at the next
publication rather than recursively.

## 5.3.3.2 Publication is atomic

A layer is not merely a name and a precedence. The published unit is
three things together: the layer table entry, the metadata key's GUID,
and the cached Security Descriptor of that key. All three are written
under one lock, and a snapshot reader that finds them incomplete
returns `EIO` rather than a half-populated layer.

A layer that has no metadata key GUID and no authorisation descriptor
is not visible in the table at all. There is no window in which a layer
exists but nobody can be authorised against it.

Creating the metadata key is ordinary key creation, so LCS computes its
descriptor from parent inheritance through KACS before the source
persists it. On the normal path the key therefore has a descriptor
before the layer can be published.

## 5.3.3.3 When the refresh runs

Changes under `Layers\` mark the affected layer names dirty. After the
mutating operation commits, and **before the syscall returns to
userspace**, LCS runs a bounded refresh for those names: it reads the
committed metadata key, its values and its descriptor, and publishes
the new entry atomically.

For a transaction the refresh runs once, after the source commit
succeeds and before `REG_IOC_COMMIT` returns.

The internal self-watch is what notices the subtree changed, but the
watch callback is not the atomicity boundary and must never publish a
partial entry. LCS does not perform source round trips while holding
the watch-map or layer-table publication locks.

## 5.3.3.4 When the metadata descriptor will not parse

If the metadata key's descriptor cannot be read or parsed during a
refresh, the source has returned malformed data. LCS emits an audit
event, does not publish or update that layer's entry, and keeps the
previous known-good one. If the refresh was required to complete the
operation in hand — creating a layer, say, or exposing one — the
syscall fails with `EIO`.

---

# 5.3.4 Writing Into a Layer

_Peios / Advanced Peios / PKM / LCS / Layers_

> Every mutation targeting a layer requires layer write access — the base layer before it exists, the precedence gate, and deleting a layer.

Every mutating operation that targets a layer — value writes, value
deletions, tombstones, key hides, blanket tombstones, key creation —
requires **layer write authorization**: `KEY_SET_VALUE` on the layer's
metadata key at `Machine\System\Registry\Layers\<LayerName>\`.

This is a second AccessCheck, against a different object, and it is in
addition to the fd's granted mask on the target key. Both must pass.

The descriptor on a layer's metadata key is therefore the answer to
"who may write into this layer". The base layer's inherits from the
`Machine` hive root — SYSTEM and Administrators with `KEY_ALL_ACCESS`.
Group Policy layers get restrictive descriptors from the GP client at
creation; role layers get theirs from the role installer.

That closes two escalation paths at once. An unprivileged process
cannot write into a GP layer, and one role's service cannot write into
another role's.

The layer metadata descriptors are cached alongside the layer table and
invalidated by the same self-watch (§5.3.3). A layer that is not in the
table is `ENOENT` for any operation naming it.

## 5.3.4.1 The base layer before it exists

On first boot, before seed restore, `Layers\base\` does not exist. LCS
falls back to a compiled-in default descriptor granting
`KEY_ALL_ACCESS` to SYSTEM and Administrators, so base-layer writes
work from the start. It is replaced by the persisted descriptor the
moment seed restore creates the key.

## 5.3.4.2 Layer lifecycle

| Operation | Requirement |
|---|---|
| Create a layer at precedence 0 | `KEY_CREATE_SUB_KEY` on `Layers\` |
| Create a layer above precedence 0 | `KEY_CREATE_SUB_KEY` on `Layers\` **and** `SeTcbPrivilege` |
| Write into a layer | `KEY_SET_VALUE` on the layer's metadata key |
| Modify layer metadata | `KEY_SET_VALUE` on the metadata key; raising precedence above 0 additionally requires `SeTcbPrivilege` |
| Delete a layer | `DELETE` on the metadata key's fd |

Everything except the precedence rule is controlled purely by the
descriptor on the metadata key.

## 5.3.4.3 The precedence gate

`SeTcbPrivilege` is required specifically to establish or raise a
layer's precedence above 0. It is defence in depth: compromising the
descriptor on `Layers\` is not enough to create a Group Policy-tier
layer.

The check is synchronous and inline at `REG_IOC_SET_VALUE` time, and it
happens early — before sequence allocation, before transaction
enlistment, before the source is contacted. It runs when three things
hold: the target key GUID is in the set of known layer metadata keys,
the value name folds equal to `Precedence` under the same Unicode
folding used for every other value name, and the data is a positive
`REG_DWORD`. Failing the privilege check is `EPERM`.

The gate tests for a four-byte `REG_DWORD` specifically. A `Precedence`
written with some other type slips past it — and then fails at the
refresh, which rejects a non-`REG_DWORD` `Precedence` as malformed
metadata. The precedence never actually rises.

`REG_IOC_RESTORE` has its own equivalent gate, applied to the backup
stream's layer manifest before anything is written (§5.9.3).

## 5.3.4.4 Deleting a layer

Deleting the metadata key fires a `SUBKEY_DELETED` on the internal
self-watch. LCS removes the layer from the table and broadcasts
`RSI_DELETE_LAYER` to every registered source, each of which purges
every entry tagged with that name and reports the GUIDs that lost their
last path entry.

Before the broadcast, LCS aborts every bound transaction whose mutation
log touched that layer (§5.2.9).

A `SUBKEY_DELETED` for `base` is ignored (§5.3.2).

---

# 5.3.5 Private Layers

_Peios / Advanced Peios / PKM / LCS / Layers_

> A disabled layer attached to a thread's credentials — how it resolves, how it is attached, and the privilege that is deliberately not checked.

A private layer is a **disabled** layer attached to a thread's
credentials. It is invisible during ordinary resolution and treated as
enabled when resolving on behalf of a thread whose token names it.

That covers three things a shared registry otherwise cannot do: giving
one session experimental settings without affecting others; injecting
test configuration without touching the shared tree; and giving a
container a different view of the registry without a separate hive.

## 5.3.5.1 Resolution

A private layer participates in normal precedence ordering. A disabled
layer with precedence 5 attached to a thread resolves at precedence 5,
competing with everything else at that level. It is not an overlay on
top; it is a layer that only that thread can see.

The activity test is exactly: a layer is active for a thread if it is
globally enabled, or its name appears in that thread's private layer
set. Name matching uses Unicode Simple Case Folding, like every other
layer name comparison.

## 5.3.5.2 Attachment

Private layer names reach a thread through the KACS token's LCS
credential extension — the same versioned block that carries scope
GUIDs for private hives (§5.2.2). LCS reads the credentials from the
effective token on each operation and passes them into resolution.

Private layers are therefore **per-thread, not per-process**: threads
in one process can hold different private layer sets through different
impersonation tokens.

## 5.3.5.3 Two things about the caps

`MaxPrivateLayersPerToken`, default 16, is described as a limit on
attachment. It is not enforced there. KACS applies its own hard cap of
256 names when it parses the token specification, and the configurable
LCS limit is applied later, when LCS acquires a thread's private
credentials for an operation.

The consequence is that a token carrying seventeen private layers is
accepted by KACS and then fails every LCS operation, rather than being
refused when it was built. Reading LCS's configured limits from KACS
would invert the dependency between the two, so the cap stays where it
can be read.

The failure is `E2BIG`. It was `EACCES`, which read as an access-control
denial and sent anyone debugging it towards descriptors and privileges
rather than towards a count that was fixed when the token was assembled,
possibly in another process. `MaxScopeGUIDsPerToken` shares the check
and the errno. A missing token is still `EACCES`, because that one is an
access decision.

KACS also deduplicates private layer names using ASCII case-insensitive
comparison, where LCS matches them with Unicode Simple Case Folding.
Two names that LCS would treat as one layer can both sit on a token.

## 5.3.5.4 The privilege that is not checked

Attaching a private layer whose precedence is above 0 ought to require
`SeTcbPrivilege` — otherwise an unprivileged process can attach an
existing high-precedence disabled layer to its own credentials and see,
and potentially influence, Group Policy-tier configuration.

**That check does not exist.** KACS never consults the LCS layer table
when parsing the credential extension, and there is no precedence
lookup and no privilege test anywhere on the attachment path. What does
gate attachment is `SeCreateTokenPrivilege`, because private layer
names can only enter a token when the token is created — the same
blanket gate that governs scope GUIDs.

---

# 5.3.6 Resolution

_Peios / Advanced Peios / PKM / LCS / Layers_

> Turning several per-layer entries into one effective answer — the rule, why unknown layers are latent rather than wrong, and enumeration.

Layer resolution turns several per-layer entries into one effective
answer. It is the mechanism that makes the registry layered, and it is
the same algorithm for path entries and for values — one resolves
existence claims, the other resolves data.

## 5.3.6.1 The rule

Every candidate is a tuple `(precedence, sequence, entry)`. The winner
is the maximum, ordered by precedence first and sequence second.

Building the candidate list:

1. For each entry the source returned, look up its layer in the current
   layer table. An entry naming a layer that is not in the table is
   **skipped**, not rejected.
2. Discard entries from layers that are not active for this thread — a
   layer is active if it is globally enabled or its name is in the
   thread's private layer set (§5.3.5).
3. For values, add every blanket tombstone on the key as a tombstone
   candidate for the requested name, at its own precedence and
   sequence (§5.2.7).
4. If there are no candidates, the answer is not-found.
5. Take the maximum. A winning HIDDEN path entry, a winning
   `REG_TOMBSTONE` value entry, and a winning blanket all mean
   not-found.

That is the whole algorithm. Everything the layer system does —
override, revert, mask, hide, hide-and-replace — is a consequence of
it.

## 5.3.6.2 Unknown layers are latent, not wrong

An entry tagged with a well-formed layer name that is not currently in
the table is a valid **latent** entry. It is ignored while the layer is
absent, and if a layer with the same folded identity is later created,
that entry becomes eligible for resolution under the new metadata.

This is what makes restore, import and boot ordering work: source
storage can legitimately hold entries before their metadata has been
loaded. It also follows the core rule — sources persist entries, LCS
decides their meaning.

Normal operations do not create such entries. A layer-targeting ioctl
naming a layer that is not in the table returns `ENOENT`. Latent
entries come from existing storage, from a restore or import, from a
previous boot, or from source behaviour. Since sources are trusted,
a well-formed latent entry is not malformed merely because its layer is
absent right now.

An entry whose layer *name* is malformed is a different matter, and is
rejected as malformed source data.

## 5.3.6.3 Enumeration

Enumerating values collects the unique names across all layers and
resolves each one, returning only those whose answer is not not-found.
Enumerating subkeys collects the unique child names and resolves each,
returning those that map to a GUID rather than HIDDEN.

"Unique" means folded-unique. Two entries whose names differ only in
case are one name, which is the only reading consistent with names
being case-insensitive.

A caller sees effective state only. Tombstoned values, blanket-masked
values and hidden keys are simply absent. Per-layer raw data is never
visible through a normal operation.

## 5.3.6.4 Enumeration is index-based, and that has a cost

`REG_IOC_ENUM_VALUES` and `REG_IOC_ENUM_SUBKEYS` return one entry at an
index. Each call re-resolves the full set and returns the entry at that
position, so walking `0..N-1` performs N full resolutions — O(n²) work
overall.

For a key with a handful of values that does not matter.
`REG_IOC_QUERY_VALUES_BATCH` exists for when it does: one call, the
whole effective value set, one resolution.

Enumeration order is not defined, and the index-to-entry mapping can
change between calls if the effective set changes. Indices must not be
cached across mutations. The batch call is also the way to get a
consistent snapshot.

## 5.3.6.5 Neither party can lie about ordering

Two rules stop a source manipulating the outcome.

A source-returned entry whose sequence number is greater than or equal
to the next sequence LCS would allocate is **malformed data**. Sources
store the numbers LCS assigns them; they cannot legitimately hold a
future one. Without this, a compromised source could fabricate a
sequence number and win every tie in its own hive. Every entry in a
response is validated this way, whatever layer it names.

Duplicate sequence numbers at the same precedence, where they would
actually have to be compared to pick a winner, are also malformed. LCS
rejects the response rather than making an arbitrary choice. Duplicates
that never get compared are not an error.

Both produce `EIO` and an `LCS_SOURCE_VALIDATION_FAILURE` audit event
(§5.4.4).

---

# 5.3.7 The Sequence Counter

_Peios / Advanced Peios / PKM / LCS / Layers_

> The single global monotonic counter stamped on every layer-qualified entry — allocation, initialisation, and what it is not.

LCS keeps one global monotonic counter. Every mutation that creates a
layer-qualified entry — a path entry, a value write, a key hide, a
blanket tombstone — takes the next number from it. The counter is never
decremented and never reset.

It provides deterministic tiebreaking within a precedence tier
(§5.3.6). Wall-clock time is tracked separately, as each key's last
write time, for humans.

## 5.3.7.1 Allocation

Allocating a number increments the counter. **Allocated numbers are
never reused**, even if the operation later fails, times out, or is
part of a transaction that aborts. Gaps in the sequence space are
normal and carry no meaning.

A transactional mutation is assigned its number when the operation is
**accepted into the transaction**, not at commit. That preserves the
order in which the caller performed the operations, which is what layer
tiebreaking and watch ordering need if it commits (§5.7.2).

## 5.3.7.2 Initialisation

Each source reports the highest sequence number it has persisted in its
registration handshake, and LCS raises the counter to one above the
maximum reported. New writes therefore always outrank anything already
in storage, even after a restart.

A source registering later advances the counter the same way, to
`max(current, source_max + 1)`. If that addition would overflow 64 bits
the registration fails with `EOVERFLOW` and the source is not made
Active — the failure happens before the slot becomes usable.

The counter itself refuses to hand out `U64_MAX`, so the value is never
allocated.

## 5.3.7.3 What a sequence number is not

A sequence number is not a hive generation number.

A **sequence number** orders layer-qualified entries for resolution and
is persisted by sources.

A **hive generation number** is a volatile, per-hive, kernel-owned
change epoch, exposed by `REG_IOC_QUERY_KEY_INFO` (§5.5.3) so that a
watcher recovering from `OVERFLOW` can tell whether it actually missed
anything. Sources never see it and never persist it. Its baseline is
initialised from the source's reported maximum sequence at
registration, purely so that observed generations are monotonic
relative to persisted entries; after that the two are unrelated.

## 5.3.7.4 Restore

A restore does not write the backup's sequence numbers. It reserves a
fresh range and remaps into it, so restored entries are newer than
everything that was there before while keeping their relative order
from the backup (§5.9.3).

---

# 5.4.1 The Access Flow

_Peios / Advanced Peios / PKM / LCS / Security_

> The registry's security model is KACS applied to keys — no traverse checking, symlink handling, and why an fd is a capability.

The registry's security model is KACS applied to keys. LCS defines no
access control mechanism of its own: the same AccessCheck, the same
Security Descriptors, the same tokens and SIDs that every other Peios
subsystem uses. What this section describes is how those primitives
attach to registry operations.

Every open follows the same five steps.

1. **Token capture.** LCS takes the calling thread's effective token —
   the impersonation token if one is set, otherwise the process primary
   token. This is the same capture KACS performs for every syscall.

2. **Path resolution.** LCS walks the path through the layer stack,
   following symlinks. **No access check happens during the walk.**
   Intermediate keys are not evaluated; only the final key matters.

3. **AccessCheck.** LCS calls KACS AccessCheck with the captured token,
   the final key's Security Descriptor as returned by the source, and
   the desired access mask. Every requested right must be granted or
   the open fails with `EACCES`. There is no partial grant: the caller
   gets what it asked for or nothing.

   `MAXIMUM_ALLOWED` is the exception, and the only way to ask for
   whatever is available. AccessCheck computes the full allowed set and
   that becomes the granted mask.

4. **The granted mask is stored on the fd.** It never changes.

5. **Per-ioctl checks are bitmask tests.** Each ioctl has a required
   right; LCS tests the fd's granted mask against it and returns
   `EACCES` without contacting the source if it is absent. The
   Security Descriptor is not re-read and AccessCheck is not
   re-evaluated.

## 5.4.1.1 No traverse checking

LCS checks nothing on the way down. A process can open
`Machine\System\Services\Jellyfin` without holding any access to
`Machine\System\Services` or `Machine\System`.

This matches the Windows registry and is a deliberate difference from
filesystem path semantics, where every directory in a path is checked
for traverse. It means a key's Security Descriptor is the whole story
about who can reach it, and that an ancestor's descriptor confers no
protection on its descendants.

## 5.4.1.2 Symlinks

Opening a symlink follows it, and AccessCheck runs on the **target**
key, not the link. `REG_OPEN_LINK` opens the link itself instead — but
only for the final path component. A symlink encountered part-way
through a path is followed regardless.

Creating a symlink is privileged: `KEY_CREATE_SUB_KEY` and
`KEY_CREATE_LINK` on the parent, plus either `SeTcbPrivilege` or
membership of Administrators (§5.2.4).

## 5.4.1.3 Fds are capabilities

A key fd can be passed over a Unix socket with `SCM_RIGHTS`, and it
carries its granted mask with it. The recipient gets the access the
original opener was granted, whether or not its own token would have
passed AccessCheck.

This is explicit delegation and it is consistent with how fds work
everywhere else in Peios. Passing a `KEY_WRITE` fd hands over write
access.

Opening relative to a parent fd skips path parsing and AccessCheck for
the parent portion — the caller already proved its access when it
obtained the parent fd. This is the ordinary way to traverse a subtree.

## 5.4.1.4 Changing a descriptor does not revoke a handle

A Security Descriptor change takes effect for future opens. An fd that
already exists keeps the mask it was granted at open, because that is
what semantic rule 5 says (§5.1). The recourse for genuinely revoking
access is to restart the process holding the fd.

Descriptor changes are also not layer-qualified. They are direct
mutations on the key object, and deleting a layer does not undo one
(§5.1, rule 4).

## 5.4.1.5 The second check nobody expects

Every layer-qualified mutation runs a **second** AccessCheck, against a
different object: the metadata key of the layer being written to. That
check is for `KEY_SET_VALUE` and it is in addition to the fd's granted
mask on the target key. Both must pass. §5.3.4 covers it.

---

# 5.4.2 Access Rights

_Peios / Advanced Peios / PKM / LCS / Security_

> Registry rights at their Windows bit positions — the convenience masks, generic mapping, and validation of what a caller asks and a source returns.

Registry rights occupy the Windows bit positions, so a Security
Descriptor carrying registry ACEs is binary-compatible with one written
by Windows tooling. That is a `registry.pol` and Samba requirement, not
an aesthetic choice.

The values are in §5.A. In summary: six specific rights in bits 0–5
(`KEY_QUERY_VALUE`, `KEY_SET_VALUE`, `KEY_CREATE_SUB_KEY`,
`KEY_ENUMERATE_SUB_KEYS`, `KEY_NOTIFY`, `KEY_CREATE_LINK`), the four
standard rights `DELETE`, `READ_CONTROL`, `WRITE_DAC` and
`WRITE_OWNER`, and `ACCESS_SYSTEM_SECURITY` for the SACL, which is
itself gated by `SeSecurityPrivilege`.

There is no execute right on a key.

## 5.4.2.1 Convenience masks and generic mapping

`KEY_READ`, `KEY_WRITE` and `KEY_ALL_ACCESS` are concrete masks, not
generic bits — they are unions of the rights above and are usable
directly.

Separately, LCS accepts the raw KACS generic bits `GENERIC_READ`,
`GENERIC_WRITE`, `GENERIC_EXECUTE` and `GENERIC_ALL` in a caller's
`desired_access` and in ACE masks in a Security Descriptor. Those are
mapped through the registry generic mapping before AccessCheck sees
them. `GENERIC_READ` maps to `KEY_READ`, `GENERIC_WRITE` to
`KEY_WRITE`, `GENERIC_ALL` to `KEY_ALL_ACCESS`, and **`GENERIC_EXECUTE`
maps to zero**, because there is nothing to execute.

## 5.4.2.2 Validating what a caller asks for

`desired_access` is validated before path resolution or AccessCheck.

- Zero is `EINVAL`. A caller must ask for something.
- Any bit outside the valid caller mask is `EINVAL`.
- `MAXIMUM_ALLOWED` may appear alone or combined with anything else.
- `SYNCHRONIZE` (`0x00100000`) is not a registry right, so it is an
  unknown bit and fails `EINVAL`.

The valid caller mask is a single constant, `REG_VALID_DESIRED_ACCESS_MASK`
(§5.A): the six specific rights, the four standard rights,
`ACCESS_SYSTEM_SECURITY`, `MAXIMUM_ALLOWED` and the four generic bits.

## 5.4.2.3 Validating what a source returns

Source-supplied Security Descriptors are validated too, because a
source is trusted for its data but not for its arithmetic (§5.8.4).

An ACE mask may contain concrete registry rights and raw generic bits.
It **may not** contain `MAXIMUM_ALLOWED` — that is a request, not a
grant, and it is meaningless in an ACE. After generic mapping, an ACE
mask must be a subset of the concrete registry rights plus
`ACCESS_SYSTEM_SECURITY`.

A descriptor that breaks either rule is malformed source data: the
operation fails closed with `EIO` and an
`LCS_SOURCE_VALIDATION_FAILURE` audit event is emitted (§5.4.4).

Two further constants, `REG_VALID_MAPPED_ACCESS_MASK` and
`REG_VALID_ACE_ACCESS_MASK`, express those two bounds.

## 5.4.2.4 Which right each operation needs

| Operation | Required |
|---|---|
| `REG_IOC_QUERY_VALUE`, `QUERY_VALUES_BATCH`, `ENUM_VALUES` | `KEY_QUERY_VALUE` |
| `REG_IOC_SET_VALUE`, `DELETE_VALUE`, `BLANKET_TOMBSTONE`, `FLUSH` | `KEY_SET_VALUE` |
| `REG_IOC_ENUM_SUBKEYS` | `KEY_ENUMERATE_SUB_KEYS` |
| `REG_IOC_QUERY_KEY_INFO` | `READ_CONTROL` |
| `REG_IOC_DELETE_KEY`, `HIDE_KEY` | `DELETE` |
| `REG_IOC_NOTIFY` | `KEY_NOTIFY` |
| `REG_IOC_GET_SECURITY` | `READ_CONTROL` for owner, group and DACL; `ACCESS_SYSTEM_SECURITY` for the SACL |
| `REG_IOC_SET_SECURITY` | `WRITE_OWNER` for owner **or group**; `WRITE_DAC` for the DACL; `ACCESS_SYSTEM_SECURITY` for the SACL |
| `REG_IOC_BACKUP` | `SeBackupPrivilege`, no per-key check |
| `REG_IOC_RESTORE` | `SeRestorePrivilege`, no per-key check |
| creating a key | `KEY_CREATE_SUB_KEY` on the parent |
| creating a symlink key | `KEY_CREATE_SUB_KEY` and `KEY_CREATE_LINK` on the parent, plus `SeTcbPrivilege` or Administrators |

Where a `REG_IOC_SET_SECURITY` or `GET_SECURITY` request names several
components, every right those components imply must be present before
the source is contacted.

`REG_IOC_FLUSH` requires `KEY_SET_VALUE` because a flush is only
meaningful to a caller that wrote something and wants it durable.
Requiring a write right stops an unprivileged reader using flush as a
disk-I/O amplifier.

## 5.4.2.5 Enumeration exposes names, not contents

`REG_IOC_ENUM_SUBKEYS` performs no per-child access check. Every
visible child is returned, whatever the caller's access to it. The
caller learns names, and must open each child separately — with a real
AccessCheck — to read anything.

Subtree watches work the same way: a watcher is told a descendant was
created without a check on that descendant. Structure visibility is
deliberately weaker than content visibility, matching
`RegNotifyChangeKeyValue` and `RegEnumKeyEx`.

---

# 5.4.3 Inheritance and Hive Roots

_Peios / Advanced Peios / PKM / LCS / Security_

> A new key's descriptor is computed from its parent by the KACS inheritance algorithm — plus hive roots and reading or writing a descriptor.

## 5.4.3.1 Inheritance at creation

When `reg_create_key` creates a key, its initial Security Descriptor is
computed from the parent's by the KACS inheritance algorithm. LCS
supplies the parent descriptor, the creating token, the registry
generic mapping and the valid-mask bound, and hands the result to the
source to persist. It implements no inheritance logic of its own.

Three things about registry inheritance are worth stating.

**It is static.** The computation happens once, at creation. A later
change to the parent's descriptor does not propagate to children that
already exist. Re-propagating is an explicit administrative action — a
client-side tree walk — not a kernel operation, and there is no code
anywhere in LCS that walks a tree to re-propagate.

**Only `CONTAINER_INHERIT_ACE` matters.** Every registry object is a
container: keys hold subkeys and values. Values are not independent
security objects and have no descriptors of their own; they inherit
their key's access control. `OBJECT_INHERIT_ACE` is never used to
select an ACE for inheritance. It is only cleared on the child copy
when `NO_PROPAGATE_INHERIT_ACE` applies.

**A parent with no inheritable ACEs falls back to the creating token's
default DACL.** That fallback covers the DACL only; there is no default
SACL.

## 5.4.3.2 Hive roots

A hive root has no parent, so it is the top of every inheritance chain
below it and cannot inherit anything itself. Its descriptor is created
by the **source**, on first boot, and LCS enforces whatever the source
stored.

LCS holds no template. There are no hardcoded SIDs, no default hive
root descriptors, and no code that would construct one — searching for
them finds nothing. The defaults that follow are what loregd writes;
they are conventions of the source, not properties of the kernel.

**`Machine\`:**

| Principal | Rights | Inheritance |
|---|---|---|
| SYSTEM | `KEY_ALL_ACCESS` | Container-inherit |
| Administrators | `KEY_ALL_ACCESS` | Container-inherit |
| Authenticated Users | `KEY_READ` | Container-inherit |

**`Users\<SID>\`:**

| Principal | Rights | Inheritance |
|---|---|---|
| the user's SID | `KEY_ALL_ACCESS` | Container-inherit |
| SYSTEM | `KEY_ALL_ACCESS` | Container-inherit |
| Administrators | `KEY_ALL_ACCESS` | Container-inherit |

These mirror Windows HKLM and HKU. A subsystem needing something
tighter — `Machine\Security\`, say — sets an explicit descriptor on its
own subtree root at creation, overriding what it inherited.

Because there is no traverse checking (§5.4.1), a restrictive
descriptor high in the tree protects only the key it is on. Protection
of a subtree comes from the descriptors its keys inherited at creation,
which is exactly why an administrator changing a parent's descriptor
and expecting the subtree to follow will be disappointed.

## 5.4.3.3 Reading and writing descriptors

`REG_IOC_GET_SECURITY` and `REG_IOC_SET_SECURITY` take a
`security_info` bitmask naming which components to act on: owner,
group, DACL, SACL. Zero is `EINVAL`, and so is any unknown flag; both
are rejected before the source is contacted, before transaction
enlistment and before any mutation.

The rights required are computed from every component named. Reading
owner, group or the DACL needs `READ_CONTROL`; reading the SACL needs
`ACCESS_SYSTEM_SECURITY`. Setting owner **or group** needs
`WRITE_OWNER`; setting the DACL needs `WRITE_DAC`; setting the SACL
needs `ACCESS_SYSTEM_SECURITY`. A request naming several components
must hold all the corresponding rights.

A set is a merge, not a replacement. LCS reads only the components
`security_info` names from the supplied self-relative descriptor and
preserves the existing ones. The result must still have an owner; a
merge that would leave the descriptor ownerless is `EINVAL`. A null
group SID stays valid.

Enlisting a descriptor change in a transaction gives it atomicity with
the rest of the transaction's operations. It does not make it
layer-qualified: the change is still a direct mutation on the key, is
still not reverted by deleting a layer, and is simply not applied at
all if the transaction aborts.

---

# 5.4.4 Audit

_Peios / Advanced Peios / PKM / LCS / Security_

> The seven audit events LCS emits, which are unconditional, and what happens when emission itself fails.

LCS emits audit events through KMES. Seven events exist.

| Event | Emitted when |
|---|---|
| `LCS_KEY_OPEN_AUDIT` | A key open matched a SACL audit ACE. |
| `LCS_BACKUP_START` | Before `REG_IOC_BACKUP` reads any subtree data. |
| `LCS_BACKUP_COMPLETE` | After a backup completes or fails after starting. |
| `LCS_RESTORE_START` | Before `REG_IOC_RESTORE` modifies any source state. |
| `LCS_RESTORE_COMPLETE` | After a restore completes or fails after starting. |
| `LCS_SOURCE_VALIDATION_FAILURE` | LCS rejected malformed source data. |
| `LCS_SELF_CONFIG_INVALID` | LCS rejected an invalid self-configuration value. |

Backup and restore are audited **unconditionally**, whatever the SACL
on the target key says. They are privilege-gated bulk operations that
bypass per-key access checks entirely, so the audit trail is the only
record that they happened.

Every payload is a single MessagePack map with string keys. GUIDs are
16-byte binary values; SIDs are binary KACS encodings.

## 5.4.4.1 The caller summary

Six of the seven carry a `caller` submap describing the effective
token used for the operation. It has nine fields and no more:
`effective_token_guid`, `true_token_guid`, `process_guid` and
`user_sid`, then `authentication_id`, `token_id`, `token_type`,
`impersonation_level` and `integrity_level`.

The bound is deliberate. Group lists, privilege arrays, claims and
default DACLs are unbounded and are never included. The summary carries
enough to correlate an event with a caller, and nothing that could make
one event arbitrarily large. A primary token reports an impersonation
level of 0.

## 5.4.4.2 Key opens

`LCS_KEY_OPEN_AUDIT` carries the caller summary, the key GUID, the
requested and granted access masks, the decision (`allowed` or
`denied`) and `sacl_match_flags` — bit 0 for a success-audit match, bit
1 for a failure-audit match, no other bits.

`granted_access` is forced to zero on a denial, and that is enforced
rather than merely intended: a denied event carrying a non-zero granted
mask is rejected as a malformed payload. `requested_access` is the mask
after registry generic mapping, with `MAXIMUM_ALLOWED` re-added if the
caller asked for it.

SACL evaluation follows the KACS AccessCheck algorithm — the SACL is
evaluated alongside the DACL, not separately. Reading or modifying a
SACL requires `ACCESS_SYSTEM_SECURITY`, which is itself gated by
`SeSecurityPrivilege`.

A request of `MAXIMUM_ALLOWED` **alone** maps to a desired mask of zero,
so AccessCheck's SACL walk matches each audit ACE against the *granted*
mask instead. An audit ACE says "audit when someone gets this right",
and with `MAXIMUM_ALLOWED` they did get it. Such an open therefore
always audits as a success, which is correct: `MAXIMUM_ALLOWED` returns
whatever is available and never fails, so a failure ACE has nothing to
record. An ACE naming a right the caller did not receive still does not
match.

## 5.4.4.3 Source validation failures

`LCS_SOURCE_VALIDATION_FAILURE` carries the source slot identifier and
then, where each is known, the hive name, the RSI request id, the
operation code and the key GUID. The last field, `validation_class`,
names what was wrong. There are twelve:

`malformed_security_descriptor`, `malformed_layer_name`,
`unknown_rsi_status_code`, `future_sequence_number`,
`duplicate_winning_sequence_tie`,
`malformed_layer_metadata_security_descriptor`,
`malformed_key_name`, `malformed_value_name`,
`malformed_response_payload`, `malformed_key_metadata`,
`malformed_value_payload`, `malformed_delete_layer_orphan_list`.

The three name classes are field-specific: layer-name fields, key
component or child-name fields, and value-name fields respectively.
The structural classes cover a response whose operation-specific
payload has the wrong shape or trailing bytes; a lookup or
enumeration whose metadata block is incomplete, duplicated,
unreferenced or nil; a value payload with an invalid type, a
tombstone/data mismatch or oversized data; and an invalid orphan GUID
array from `RSI_DELETE_LAYER`.

## 5.4.4.4 Configuration

`LCS_SELF_CONFIG_INVALID` carries the parent path and value name of
the offending parameter, the expected type and numeric range, what was
actually received — one of `missing`, `wrong_type` or
`dword_out_of_range`, with the actual type or value where applicable —
and the value LCS retained instead.

Because `missing` counts as invalid, a first boot before seed restore
emits one of these per parameter on each refresh: nineteen events
against an empty `Registry\` key. That is correct and expected, but it
is a noticeable share of the boot audit stream.

## 5.4.4.5 What happens when emission fails

The policy differs per event, and the differences are the point.

- **`LCS_KEY_OPEN_AUDIT`.** If LCS cannot *construct* a valid payload —
  corrupt internal state, allocation failure, anything on the LCS side
  — the open fails with `EIO` and no key fd is published. If the
  payload is valid but KMES cannot *retain* the event — unavailable,
  ring drops, capacity pressure, no consumer — the access decision and
  the fd publication are unaffected. Loss accounting is KMES's problem.
- **`LCS_BACKUP_START`, `LCS_RESTORE_START`.** Emission failure returns
  `EIO` and the operation does not start. Nothing is read and nothing
  is written.
- **`LCS_BACKUP_COMPLETE`, `LCS_RESTORE_COMPLETE`.** The operation has
  already finished. Emission is attempted; failure does not change the
  result.
- **`LCS_SOURCE_VALIDATION_FAILURE`.** The triggering operation is
  already failing with `EIO`. Emission is attempted; failure does not
  change that.
- **`LCS_SELF_CONFIG_INVALID`.** The invalid value has already been
  ignored and the previous known-good value retained. Emission is
  attempted; failure leaves the retained configuration in force.

The rule underneath all five: an audit failure blocks an operation only
where the audit record is the *point* of the operation being permitted.
A privileged bulk export whose start could not be recorded does not
happen. A key open whose decision could not be recorded does not
happen. Everything downstream of an already-determined outcome records
what it can.

LCS constructs the payload and attempts to enqueue it before
continuing past the audit point. It never waits for a userspace
consumer to observe or retain the event.

---

# 5.5.1 The Fd Model

_Peios / Advanced Peios / PKM / LCS / The Syscall Interface_

> The hybrid syscall and ioctl interface — key fds, open-time checking, transaction fds, and the conventions for strings and output buffers.

LCS uses a hybrid syscall/ioctl interface, the same shape KACS uses
within PKM.

**Syscalls** create file descriptors: opening a key, creating a key,
beginning a transaction. There are three, numbered 1100 to 1102 in the
PKM range. **Ioctls** operate on a descriptor that already exists —
eighteen of them, all under type byte `'R'`. **`close()`** releases
both kinds of fd through the ordinary fd lifecycle.

## 5.5.1.1 Key fds

A key fd is an anonymous inode created with `O_CLOEXEC`, holding:

| Field | Description |
|---|---|
| Source and key GUID | The identity of the opened key. |
| Granted access mask | Computed once by AccessCheck at open. Immutable. |
| Resolved path | After symlink resolution and `CurrentUser\` rewriting. |
| Ancestor chain | The GUID at each path component from the hive root down. Captured during the open walk, used for subtree watch dispatch (§5.6.3). |
| Watch state | Armed or not, filter, subtree flag, pending event queue. |

Key fds behave like any other fd: `close()`, close-on-exec,
`poll`/`epoll`, and passing over Unix sockets with `SCM_RIGHTS`. That
last one is what makes them capabilities (§5.4.1).

## 5.5.1.2 Open-time checking

A caller names a desired access mask, and AccessCheck evaluates it
against the key's Security Descriptor. All of it is granted or the open
fails with `EACCES`. `MAXIMUM_ALLOWED` is the only way to ask for
whatever is available.

The granted mask lives on the fd, and every subsequent ioctl is a
bitmask test against it — not a fresh AccessCheck, and not a fresh read
of the descriptor.

Opening relative to a parent fd skips both path parsing and
AccessCheck for the parent portion. The caller already proved its
access when it obtained the parent fd, and this is the ordinary way to
walk a subtree.

## 5.5.1.3 Transaction fds

A transaction fd is an anonymous inode holding a transaction id and,
once bound, its source and hive. Transaction lifetime is fd lifetime:
closing without committing aborts (§5.7.1).

## 5.5.1.4 Reserved fields

Every syscall and ioctl argument structure uses natural C layout with
fixed-width fields and explicit padding. Nothing is packed.

Fields named `_pad`, and anything else described as reserved, are ABI
extension points. **A caller must set them to zero**, and a non-zero
reserved or padding field fails the operation with `EINVAL` — before
source dispatch, before transaction enlistment, before sequence
allocation, and before any output is copied.

In the other direction, LCS zeroes every reserved and padding byte of
an output structure or a watch event before copying it to userspace.

Flags fields carry only the bits defined for them. An unknown or
reserved flag bit is `EINVAL` unless a specific field says otherwise.

The point of all this is that a future version can give a reserved
field meaning without an older kernel having silently accepted a value
it did not understand.

## 5.5.1.5 Strings

Strings in ioctl structures are **length-delimited, not
null-terminated**. Each is a `(len, ptr)` pair where `len` is a byte
count and `ptr` is a `u64` userspace address. LCS reads exactly `len`
bytes. A terminator is neither required nor expected, and one included
in the length is a null byte and therefore invalid.

Syscall paths are the exception: they arrive as null-terminated C
strings and have the terminator stripped before validation (§5.2.8).

## 5.5.1.6 Variable-size output buffers

Six ioctls return variable-size data — `REG_IOC_QUERY_VALUE`,
`QUERY_VALUES_BATCH`, `ENUM_VALUES`, `ENUM_SUBKEYS`, `QUERY_KEY_INFO`
and `GET_SECURITY` — and all six use one convention.

For each output buffer described by `(length, pointer)`:

- **length 0 is a size probe**, whether the pointer is null or not. The
  pointer is not dereferenced.
- **length greater than 0** requires a non-null pointer writable for
  that many bytes, or the ioctl returns `EFAULT`.

If any output buffer is too small the ioctl returns `ERANGE` and writes
**every required size it can determine**, not just the first one that
failed — so a caller with two undersized buffers learns both sizes from
one call.

On `ERANGE`, output buffers are **not partially filled**; their
contents are unspecified. Output scalar metadata is meaningful only on
success, unless an ioctl explicitly documents a field as carrying a
required size or count on `ERANGE`.

Input pointer faults return `EFAULT`, validated before source dispatch
wherever that is possible.

---

# 5.5.2 Syscalls

_Peios / Advanced Peios / PKM / LCS / The Syscall Interface_

> The three LCS syscalls — reg_open_key, reg_create_key and reg_begin_transaction — with their arguments and failure conditions.

## 5.5.2.1 `reg_open_key` (1100)

```c
int reg_open_key(int parent_fd, const char __user *path,
                 u32 desired_access, u32 flags);
```

Opens an existing key. Fails if it does not exist after layer
resolution.

| Parameter | Description |
|---|---|
| `parent_fd` | An open key fd to resolve relative to, or -1 for an absolute path. No AccessCheck is performed on the parent. |
| `path` | A null-terminated registry path — absolute with a hive prefix when `parent_fd` is -1, relative to the parent key otherwise. |
| `desired_access` | Requested rights, raw generic bits, `MAXIMUM_ALLOWED`, or a combination (§5.4.2). Zero and unknown bits are `EINVAL`. |
| `flags` | `REG_OPEN_LINK` (`0x01`) opens a symlink key rather than following it. Every other bit is reserved and must be zero. |

The open proceeds as follows.

1. Parse and canonicalise the path: normalise separators, reject empty
   components and a trailing separator, check the total length and each
   component length.
2. Rewrite a leading `CurrentUser\` to `Users\<caller SID>\` — only for
   an absolute path, and only the first component (§5.2.1).
3. Route. For an absolute path, look the hive name up, private hives
   before global ones. For a relative one, use the parent key's source
   and resolve from the parent's GUID.
4. Walk the path component by component through `RSI_LOOKUP`, resolving
   each through the layer stack and following symlinks — except a final
   component when `REG_OPEN_LINK` is set. Collect the ancestor chain as
   you go.
5. Run AccessCheck against the final key's descriptor.
6. Publish an fd holding the key GUID, the granted mask, the resolved
   path and the ancestor chain.

If the path traversed a symlink, the fd stores the *resolved* path and
ancestor chain. It refers to the target object.

| Errno | Condition |
|---|---|
| `ENOENT` | The key does not exist after layer resolution. |
| `EACCES` | AccessCheck did not grant everything requested. |
| `EINVAL` | Malformed path; zero or unknown `desired_access` bits; a symlink whose effective default value is not `REG_LINK`; maximum depth exceeded. |
| `ELOOP` | Symlink depth limit exceeded. |
| `ENAMETOOLONG` | A component or the total path is too long. |
| `ETIMEDOUT` | The source did not answer within `RequestTimeoutMs`. |
| `EIO` | The source failed or is unavailable. |
| `ENOMEM` | Kernel allocation failure. |

## 5.5.2.2 `reg_create_key` (1101)

```c
int reg_create_key(const struct reg_create_key_args __user *args);
```

Opens the key if it exists after layer resolution, creates it with an
inherited descriptor if not, and reports which happened.

| Field | Description |
|---|---|
| `parent_fd` | As `reg_open_key`. |
| `path_ptr` | Pointer to a null-terminated path. |
| `desired_access` | As `reg_open_key`. |
| `flags` | `REG_OPTION_VOLATILE` (`0x01`), `REG_OPTION_CREATE_LINK` (`0x02`). Other bits reserved. |
| `layer_ptr` | Pointer to a null-terminated layer name for creation, or null for the base layer. Ignored if the key already exists. |
| `txn_fd` | A transaction fd, or -1. A non-negative value makes creation a mutating operation that binds or reuses that transaction. |
| `disposition_ptr` | Receives `REG_CREATED_NEW` (1) or `REG_OPENED_EXISTING` (2). May be null. |
| `_pad0`, `_pad1` | Reserved; must be zero. |

**If the key exists**, this behaves as `reg_open_key`, the layer
parameter is ignored, and the disposition is `REG_OPENED_EXISTING`.

**If it does not:**

1. Resolve the parent, which must exist. Check that the parent's depth
   plus one is within `MaxKeyDepth`.
2. AccessCheck the parent for `KEY_CREATE_SUB_KEY`.
3. Perform layer write authorization against the target layer's
   metadata key (§5.3.4).
4. Mint a fresh UUIDv4 GUID.
5. Compute the new key's descriptor from the parent's, through KACS.
6. Create the path entry with a new sequence number, then the key
   record (§5.2.5).
7. AccessCheck the *new* key's inherited descriptor against
   `desired_access` — an inherited descriptor may not grant everything
   the creator asked for.
8. Publish the fd with the granted mask and disposition
   `REG_CREATED_NEW`.

Intermediate path components are not auto-created. Only the final one
is.

**Races.** If two callers race, one creates and the other observes the
key as existing. `RSI_ALREADY_EXISTS` on the path entry is retried as
an open, reporting `REG_OPENED_EXISTING`, and `EEXIST` never reaches
userspace from this syscall. `RSI_ALREADY_EXISTS` on the *key record*,
for a GUID LCS has just minted, is not a race — the source and the
kernel disagree about what exists — and fails closed with `EIO`
(§5.2.3).

| Errno | Condition, in addition to `reg_open_key`'s |
|---|---|
| `ENOENT` | The parent does not exist, or the named layer is not in the layer table. |
| `EACCES` | The parent denied `KEY_CREATE_SUB_KEY`, the inherited descriptor denied the requested access, or layer write authorization failed. |
| `EPERM` | `REG_OPTION_CREATE_LINK` without `KEY_CREATE_LINK` on the parent, or without `SeTcbPrivilege` or Administrators. |
| `ENOSPC` | The per-value layer cap was exceeded. |
| `EINVAL` | A non-volatile key under a volatile parent; a non-zero reserved field. |

## 5.5.2.3 `reg_begin_transaction` (1102)

```c
int reg_begin_transaction(void);
```

Allocates a transaction id, publishes a transaction fd in state
`REG_TXN_ACTIVE_UNBOUND`, and starts the lifetime timer. It contacts no
source and chooses none (§5.7.1). It can fail only with `ENOMEM`,
`EOVERFLOW` on transaction id exhaustion, or `EINVAL`.

---

# 5.5.3 Key Ioctls

_Peios / Advanced Peios / PKM / LCS / The Syscall Interface_

> The sixteen ioctls acting on a key fd — reading, writing, security, watches, durability and bulk — each gated on the fd's granted mask.

Sixteen ioctls act on a key fd. Each checks the fd's granted mask
first and returns `EACCES` without contacting the source if the
required right is absent (§5.4.2). Numbers, directions and argument
layouts are in §5.A.

Every mutating ioctl accepts an optional transaction fd. So do the four
read ioctls — a read inside a bound transaction sees the transaction's
own uncommitted writes. A transaction bound to a different hive is
`EXDEV`; a committed, aborted or closed one is `EINVAL`; a timed-out
one is `ETIMEDOUT`; one whose source went Down is `EIO`. An unbound
transaction fd does not bind on a read, which is simply performed
non-transactionally.

Every mutating ioctl that names a layer performs layer write
authorization first (§5.3.4).

## 5.5.3.1 Common errors

| Errno | Condition |
|---|---|
| `EACCES` | The granted mask lacks the required right, or layer write authorization failed. |
| `EFAULT` | An input pointer is invalid, or a non-zero-length output buffer pointer is null or unwritable. |
| `EIO` | The source is unavailable, failed, or the transaction's source went Down. |
| `ETIMEDOUT` | The source did not answer within `RequestTimeoutMs`, or the transaction had timed out. |
| `ENOMEM` | Kernel allocation failure. |
| `EXDEV` | The transaction is bound to a different hive. |
| `ENOENT` | The named layer is not in the layer table. |
| `EINVAL` | A non-zero reserved or padding field. |
| `ENOTTY` | An ioctl number this fd type does not implement. |

## 5.5.3.2 Reading

**`REG_IOC_QUERY_VALUE`** returns the effective value at a name:
its type, data, the sequence number of the winning entry, and the
**canonical name of the winning layer** — which is the layer table's
spelling, not whatever string the source stored. A base-layer entry
therefore reports `base`. A winning tombstone or blanket, or no
entries at all, is `ENOENT`.

**`REG_IOC_QUERY_VALUES_BATCH`** returns every effective value on the
key in one call: name, type and data for each, with tombstoned and
blanket-masked values omitted. This is the call to use when you want
them all (§5.3.6).

**`REG_IOC_ENUM_VALUES`** and **`REG_IOC_ENUM_SUBKEYS`** return the
entry at an index, and `ENOENT` past the end. Both re-resolve the full
set on every call, and enumeration order is undefined — see §5.3.6 for
why the batch call usually wins.

`ENUM_SUBKEYS` performs no per-child access check. It returns every
visible child with its last write time, subkey count and value count.
The caller learns names and must open each child separately to read
anything (§5.4.2).

**`REG_IOC_QUERY_KEY_INFO`** returns the key's name, last write time,
subkey and value counts, the maximum subkey name length, maximum value
name length and maximum value data size, the descriptor size, the
volatile and symlink flags, and the hive generation number. It requires
`READ_CONTROL`.

It is declared `_IOWR`, which is what it does: it reads the caller's
output-buffer fields out of the argument structure before writing it
back. It was declared `_IOR`, and because the direction bits are part of
the encoded ioctl number and the kernel dispatches on the whole encoded
value, correcting that was an ABI break rather than a relabelling. A
binary built against the old constant gets `ENOTTY` from a kernel
carrying the new one.

### 5.5.3.2.1 The hive generation number

A monotonic per-hive change epoch owned by the kernel. It is not a
sequence number and must not be read as one: sources neither report it
nor persist it (§5.3.7).

It is incremented once per committed mutation, or once per committed
transaction per affected hive, however many operations that transaction
contained. Its baseline is initialised from the source's reported
maximum sequence at registration so that observed values are monotonic
relative to persisted entries; saturating at `U64_MAX` is `EOVERFLOW`.

It is exposed on every key because it is cheap and because a watcher
that receives `OVERFLOW` can compare the generation it last saw with
the current one and skip the recovery re-read entirely if nothing has
committed (§5.6.4).

Layer operations produce a single generation increment covering the
metadata key deletion, `RSI_DELETE_LAYER`, the recomputation and the
resulting watch effects. There is no generation at which the metadata
key is gone but the layer's entries are still resolving. An operation
affecting several hives increments each independently.

## 5.5.3.3 Writing

**`REG_IOC_SET_VALUE`** writes a value entry in a layer. LCS allocates
a sequence number and tells the source to store
`(key GUID, value name, layer) → (type, data, sequence)`, then updates
the key's last write time.

Writing type `REG_TOMBSTONE` is the explicit tombstone operation and
requires zero-length data (§5.2.6).

A non-zero `expected_sequence` makes the write **conditional**. It is
passed to the source, which atomically verifies that the layer's own
current entry carries that sequence number before writing, and answers
`RSI_CAS_FAILED` if not — which LCS returns as `EAGAIN`. There is no
kernel-side query-then-write: the check is the source's, and it is
atomic there or nowhere.

The condition is evaluated against the **layer's own entry**, not
against the effective value. A higher-precedence layer overriding a
value is not a lost update; it is the layer system working.

Additional errors: `EINVAL` for an unknown type or a tombstone with
data; `EAGAIN` for a failed conditional write; `ENOSPC` for the layer
cap or oversized data; `ENAMETOOLONG` for the value name; `EPERM` for a
`Precedence` above 0 without `SeTcbPrivilege` (§5.3.4).

**`REG_IOC_DELETE_VALUE`** removes one layer's entry at a value name —
whether that entry was a value or a tombstone. Removing a layer's
opinion lets lower-precedence layers surface.

The operation is meant to be idempotent, and it is idempotent as far as
a caller sees, provided the source answers `RSI_OK` for an entry that
was not there. LCS does not mask a source's `RSI_NOT_FOUND`; that
propagates as `ENOENT`. Idempotency is a source obligation, not a
kernel behaviour.

**`REG_IOC_BLANKET_TOMBSTONE`** sets or removes a blanket tombstone for
a layer on this key (§5.2.7). A new sequence number is assigned for
dispatch ordering, and watch events are generated for every value whose
effective state changed.

**`REG_IOC_DELETE_KEY`** removes this key's path entry from a layer.
The parent GUID comes from the fd's ancestor chain and the child name
from the last component of its resolved path. `ENOTEMPTY` if the key
has visible children; `EINVAL` on a hive root (§5.2.9).

**`REG_IOC_HIDE_KEY`** creates a HIDDEN path entry at the same place
instead, masking lower-precedence entries. The caller must hold an open
fd to the key, which is how it proved access to it. `EINVAL` on a hive
root.

## 5.5.3.4 Security

**`REG_IOC_GET_SECURITY`** and **`REG_IOC_SET_SECURITY`** read and
merge Security Descriptor components, selected by a `security_info`
bitmask. §5.4.3 covers the rights, the merge, and the validation.

## 5.5.3.5 Watches, durability, bulk

**`REG_IOC_NOTIFY`** arms, re-arms or disarms a watch (§5.6.1).

**`REG_IOC_FLUSH`** tells the source to persist pending writes for this
key's hive, and returns when persistence is confirmed. The hive name
comes from the first component of the fd's resolved path. It requires
`KEY_SET_VALUE` (§5.4.2).

**`REG_IOC_BACKUP`** and **`REG_IOC_RESTORE`** export and replace a
subtree. They are privilege-gated with no per-key access check, and
they are covered in §5.9.

---

# 5.5.4 Transaction Ioctls

_Peios / Advanced Peios / PKM / LCS / The Syscall Interface_

> The two ioctls acting on a transaction fd — commit and status — and why the key fd error table does not apply.

Two ioctls act on a transaction fd. Everything else on that fd is
`ENOTTY` — there are no savepoint or nesting operations to have.

The common key fd error table does not apply here.

## 5.5.4.1 `REG_IOC_COMMIT`

Commits every operation in the transaction. §5.7.3 covers what happens
in each case; in summary:

| Errno | Condition |
|---|---|
| — | Success. The object becomes `COMMITTED`, poll waiters are woken, watch events are delivered, and further use of the fd returns `EINVAL`. |
| `EINVAL` | Already committed, or never bound to a source. |
| `EBUSY` | The source could not take the write lock. The transaction stays `ACTIVE_BOUND`; retry. |
| `EIO` | The source failed to commit. The transaction stays `ACTIVE_BOUND`. |
| `ETIMEDOUT` | The transaction timed out before the commit completed. |

`EBUSY` and `EIO` both leave the transaction usable: the mutation log
is retained, no events are emitted, and poll waiters are not woken as
though it had become terminal. The caller retries or closes the fd to
abort.

## 5.5.4.2 `REG_IOC_TXN_STATUS`

Reports the transaction's state and a terminal errno into a
`reg_txn_status_args`. It reads nothing from the caller, consistent
with its `_IOR` direction, and can fail only with `EFAULT` on an
unwritable output pointer.

| State | `terminal_errno` |
|---|---|
| `REG_TXN_ACTIVE_UNBOUND` | 0 |
| `REG_TXN_ACTIVE_BOUND` | 0 |
| `REG_TXN_COMMITTED` | 0 |
| `REG_TXN_ABORTED` | `EINVAL` |
| `REG_TXN_TIMED_OUT` | `ETIMEDOUT` |
| `REG_TXN_SOURCE_DOWN` | `EIO` |

For the three failed terminal states, `terminal_errno` is the errno a
further operation on the fd would return. For `COMMITTED` it is not:
the transaction reports 0, but using the fd again returns `EINVAL`.

This ioctl is what makes a poll wakeup useful. A terminal transition
reports `POLLERR | POLLHUP`, which says only that *something*
terminal happened; the status call says what.

---

# 5.5.5 The Error Model

_Peios / Advanced Peios / PKM / LCS / The Syscall Interface_

> How LCS reports failure, and the two errnos worth reading closely because they mean less than they appear to.

Syscalls and ioctls follow the ordinary Linux convention: -1 and
`errno`. The errno is the whole interface — source-specific error
detail is never surfaced to a caller.

| Errno | What it means here |
|---|---|
| `ENOENT` | A key or value does not exist after layer resolution; an enumeration index is past the end; a named layer is not in the layer table; a namespace operation on an orphaned key. |
| `EACCES` | AccessCheck denied a right, the fd's granted mask lacks it, or layer write authorization failed. |
| `EINVAL` | An invalid path or argument; a non-zero reserved or padding field; a zero or unknown-bit `desired_access`; an operation on a committed, aborted or closed transaction; a symlink target that is not `REG_LINK`; maximum key depth exceeded; delete or hide on a hive root. |
| `EFAULT` | An invalid userspace pointer. A zero-length output probe ignores its pointer; a non-zero length with a null or unwritable pointer does not. |
| `ENAMETOOLONG` | A key or value name exceeds `MaxPathComponentLength`, or a path exceeds `MaxTotalPathLength`. |
| `ELOOP` | The symlink resolution depth limit was exceeded. |
| `ETIMEDOUT` | The source did not answer within `RequestTimeoutMs`, or a timed-out transaction fd was used. |
| `EIO` | The source failed, is unavailable, returned malformed data, or a transaction's bound source went Down. |
| `ENOMEM` | Kernel allocation failure. |
| `ENOSPC` | A layer cap was exceeded, or value data exceeds `MaxValueSize`. |
| `ENOTEMPTY` | A key with visible children cannot be deleted. |
| `EXDEV` | An operation targeted a different hive from the one its transaction is bound to. |
| `EPERM` | A privilege the caller does not hold: symlink creation, creating or raising a layer above precedence 0, backup, restore. |
| `EAGAIN` | A conditional write failed — the layer entry's sequence did not match. Re-read and retry. |
| `EBUSY` | A commit could not take the source's write lock, or `MaxBoundTransactionsPerSource` or `MaxReadOnlyTransactionsPerSource` was exceeded. |
| `ERANGE` | An output buffer is too small. Every determinable required size is written; buffers are not partially filled. Retry with larger ones. |
| `EEXIST` | A path entry or key already exists at the source. |
| `ENOTSUP` | The source does not support the transaction mode being requested. |
| `EBADF` | An fd argument is not valid or not open in the required mode — a backup output fd that is not writable, a restore input fd that is not readable. |
| `EOVERFLOW` | A counter cannot advance: a source reported a persisted sequence number too large to allocate past, a hive generation saturated, restore sequence remapping would overflow, or transaction ids are exhausted. |
| `ESTALE` | A source re-registration tried to resume a Down slot with a mismatched hive identity. |

## 5.5.5.1 Two errnos worth reading closely

**`ETIMEDOUT` means "may or may not have happened."** If the deadline
expired before an in-flight RSI slot was reserved, no request was sent
at all. If it expired after dispatch, the source may still apply the
operation and answer later, and LCS will apply the kernel-side effects
when it does (§5.8.5). A caller that needs certainty checks state
before retrying. The same applies to a transaction commit.

**`EEXIST` is rarer than it looks.** It never comes out of
`reg_create_key`, which retries a losing race as an open (§5.5.2). From
`REG_IOC_RESTORE` it arrives only by propagation: the source answers
`RSI_ALREADY_EXISTS` while the stream is being replayed. There is no
kernel-side pre-check that a non-root GUID in a backup already exists
outside the subtree being replaced, so a collision is detected during
the restore rather than before it — inside the restore transaction,
which then rolls back (§5.9.3). Its other source is source
registration, where a hive identity collides with an **Active** slot;
a collision with a Down slot yields `EINVAL` or `ESTALE` instead
(§5.8.2).

---

# 5.6.1 The Watch Model

_Peios / Advanced Peios / PKM / LCS / Watches_

> A persistent subscription to changes on an open key, following inotify rather than the Windows model — what it observes, its event types and filters.

A watch is a persistent subscription to changes on an open key. It
follows the inotify model rather than the Windows one: once armed, it
stays armed until the fd closes, and events keep arriving without
re-registration. `RegNotifyChangeKeyValue` is single-shot, and the
window between receiving a notification and re-registering is a window
in which changes are missed; a persistent watch has no such window.

A watch is armed by `REG_IOC_NOTIFY` on a key fd, which requires
`KEY_NOTIFY` in the fd's granted mask. Arming takes a filter — a bitmask
of event categories — and a subtree flag. After arming, the fd is
pollable: `EPOLLIN` reports pending events, and `read()` returns
structured records.

Each fd carries at most one watch. Arming an already-armed fd replaces
the filter and the subtree setting and leaves queued events in place.
Arming with a filter of zero disarms: the watch is removed and every
pending event is discarded. To watch one key under two different
filters, open it twice.

Arming a watch on a key that is already orphaned fails with `ENOENT`. A
watch armed before the key was orphaned stays armed (§5.2.9).

## 5.6.1.1 What a watch observes

Events describe changes to **effective** state, not to layer mechanics.
A watcher sees that a value changed; it does not see which layer won,
or that a layer was deleted. Removing a layer whose value was on top
produces `VALUE_SET` for the value that surfaced underneath. Removing a
hiding entry that was concealing a lower-precedence key produces
`SUBKEY_CREATED`. The layer system is not visible through a watch at
all.

The events are computed by diffing the effective state before the
mutation against the effective state after it, which is what makes this
true by construction rather than by careful case analysis. A change
that replaces the key at a child name with a different key object —
different GUID, same name — produces `SUBKEY_DELETED` followed by
`SUBKEY_CREATED`, because that is what the diff says happened.

Only committed state is observable. Operations inside an uncommitted
transaction produce nothing; the whole set fires at commit (§5.6.4).

## 5.6.1.2 Event types

| Event | Code | Name field | Meaning |
|---|---|---|---|
| `REG_WATCH_VALUE_SET` | 1 | value name | The effective value at this name changed or appeared. |
| `REG_WATCH_VALUE_DELETED` | 2 | value name | The effective value at this name disappeared. |
| `REG_WATCH_SUBKEY_CREATED` | 3 | subkey name | A child key became visible. |
| `REG_WATCH_SUBKEY_DELETED` | 4 | subkey name | A child key became invisible. |
| `REG_WATCH_SD_CHANGED` | 5 | empty | The watched key's Security Descriptor was modified. |
| `REG_WATCH_KEY_DELETED` | 6 | empty | The watched key itself became invisible. |
| `REG_WATCH_OVERFLOW` | 7 | empty | Events were dropped; re-read to recover. |

`VALUE_SET` fires when a value is written, when a tombstone or blanket
tombstone is removed and a lower-precedence value surfaces, and when a
layer deletion makes a different value effective. `VALUE_DELETED` fires
when the last entry for a name goes away, when a tombstone masks every
entry, and when a blanket tombstone masks this name.

`SUBKEY_CREATED` and `SUBKEY_DELETED` cover both halves of the naming
model: a path entry appearing or being removed, and a hiding entry
being removed or created.

The three no-name events carry no name, and that is enforced: a record
constructed with a name for `SD_CHANGED`, `KEY_DELETED` or `OVERFLOW`
is rejected rather than emitted.

## 5.6.1.3 Filters

The filter selects event *categories*, not individual event types.

| Filter bit | Value | Admits |
|---|---|---|
| `REG_NOTIFY_VALUE` | `0x01` | `VALUE_SET`, `VALUE_DELETED` |
| `REG_NOTIFY_SUBKEY` | `0x02` | `SUBKEY_CREATED`, `SUBKEY_DELETED` |
| `REG_NOTIFY_SD` | `0x04` | `SD_CHANGED` |
| `REG_NOTIFY_ALL` | `0x07` | all three of the above |

`KEY_DELETED` and `OVERFLOW` are delivered unconditionally. They are
not in any category and no filter suppresses them: the first tells a
watcher its key is gone, and the second tells it that what it has been
told is incomplete. Neither is something a watcher can usefully opt out
of.

A filter containing an undefined bit is rejected, as is a subtree flag
other than 0 or 1 or a non-zero padding byte in the argument structure.

## 5.6.1.4 Blanket tombstones

A watcher never learns that a blanket tombstone exists. When one is
written, LCS works out which values it newly masks and emits one
`VALUE_DELETED` per name; when one is removed, one `VALUE_SET` per name
that became visible. The per-value view is the only view.

---

# 5.6.2 Event Records

_Peios / Advanced Peios / PKM / LCS / Watches_

> Events read from the key fd with read() — the record layout, why not every subtree record carries a path, and forward compatibility.

Events are read from the key fd with `read()`. A single call returns as
many complete events as fit in the caller's buffer; an event is never
split across two calls. If the buffer cannot hold even the first queued
event, `read()` fails with `EINVAL` — the buffer is too small to make
progress, and the caller has to try again with a larger one. On an
armed fd with an empty queue, `read()` blocks, or returns `EAGAIN` under
`O_NONBLOCK`.

Only events that were copied out in full are dequeued.

## 5.6.2.1 Layout

Every record begins with the same four fields.

| Offset | Size | Field |
|---|---|---|
| 0 | 4 | `total_len` |
| 4 | 2 | `event_type` |
| 6 | 2 | `name_len` |
| 8 | `name_len` | `name`, UTF-8 |

All integers are little-endian. `total_len` is the whole record
including this header; it is how a consumer advances to the next event,
and it is the only safe way to do so.

A subtree watch's records carry additional fields after the name,
locating the key the change happened on relative to the watched key:

| Size | Field |
|---|---|
| 2 | `path_depth` |
| `2 + n` each | `path_components`: `len` (u16) then that many UTF-8 bytes |

`path_depth` is the number of components from the watched key down to
the changed key; zero means the change was on the watched key itself.
The components are length-prefixed rather than joined with a separator,
because registry names can contain any Unicode character and value
names can contain backslashes — a concatenated path string would be
ambiguous.

The header offsets are ABI, named in `uapi/pkm/lcs.h` and listed in
§5.A.

## 5.6.2.2 Not every record on a subtree watch has a path

`OVERFLOW` records are emitted in the bare eight-byte form, on every
watch, whether or not that watch is a subtree watch. `KEY_DELETED` is
not: a subtree watcher receives it in the subtree form, with a
`path_depth` of its own.

A consumer of a subtree watch therefore cannot assume the subtree
fields are present on every record it reads. `total_len` is the cursor;
`path_depth` is read only when the record is long enough to hold it.

## 5.6.2.3 Length limits

`name_len` and each component length are 16-bit. If an event's name or
one of its path components is too long to be represented, LCS does not
emit a truncated or malformed record: it substitutes or preserves an
`OVERFLOW` for that watcher instead, which is a statement the consumer
already knows how to act on.

## 5.6.2.4 Forward compatibility

Future versions may append fields after `path_components`. An existing
consumer skips them, because it advances by `total_len`; a newer one
compares `total_len` against what it has parsed to discover whether the
optional fields are present. This is the whole extension mechanism, and
it is why the length field comes first.

---

# 5.6.3 Dispatch

_Peios / Advanced Peios / PKM / LCS / Watches_

> Given a mutation, which armed watches hear about it — computed from object identity and ancestry, with depth, batching and recovery.

Dispatch answers one question: given a mutation on a key, which armed
watches hear about it? The answer is computed from object identity and
from ancestry captured at open time, never from a path string resolved
at the moment of the event.

## 5.6.3.1 A watch is bound to an object

A watch is registered against the GUID on the fd it was armed through.
It fires for changes to *that object*. If a layer change causes a
different key to become visible at the same path, the watch does not
move: the new key is a different object with a different GUID, and
nothing about the old watch refers to a name.

To follow the path rather than the object, a process detects the change
— through `KEY_DELETED` on the old object, or a subtree watch on the
parent — re-opens the path, and arms a new watch. This is the fd model
applied consistently: an fd is a capability bound to one identity.

The converse also holds. If a watched key is hidden, `KEY_DELETED` is
delivered, but the watch is not removed. Should the hiding layer later
be deleted, the key reappears at its original path with the same GUID,
and events resume. There is no re-emergence event; the watcher simply
observes the key being active again.

## 5.6.3.2 The ancestor chain

Subtree dispatch needs to know where a key sits in the tree, and it
learns that once, at open.

Resolving a path walks it component by component, and the GUID at each
level is retained on the resulting fd as the **ancestor chain** —
root GUID through parent GUID to the key itself. It costs nothing
extra: the walk had to resolve those components anyway. A relative open
copies the parent fd's chain and extends it with the components the
relative walk resolved. An open that followed a symlink records the
chain of the *resolved* path, so the fd's ancestry reflects the target's
real position in the tree, not the link's.

Dispatch uses that captured chain and never re-resolves it. If an
ancestor in the chain is later hidden, deleted, orphaned, or replaced
at the same path by a different key, watches on the original ancestor
GUID still receive subtree events from descendants mutated through fds
opened through that chain. Watches on a new key that later appears at
the same path receive nothing from the old one.

## 5.6.3.3 The algorithm

Two structures back dispatch. The **watch map** is a hash map from GUID
to the watchers armed on it. The **subtree watch set** is a refcounted
hash set of the GUIDs that have at least one subtree watch, maintained
as watches are armed, re-armed, disarmed and closed.

For a mutation on key *B*, with *B*'s ancestor chain in hand:

1. Look up *B*'s GUID in the watch map and queue the event, with
   `path_depth` 0, to every watcher whose filter admits it.
2. Walk the ancestor chain outward from *B*'s parent. At each ancestor,
   skip it unless it is in the subtree watch set — that test is what
   makes the walk cheap. For an ancestor that is, queue the event to
   each of its watchers that armed with the subtree flag and whose
   filter admits it, with the path from that ancestor down to *B*.

The cost is O(depth) hash lookups, with no RSI round trips, no trie and
no string comparison. The path components handed to a subtree watcher
are sliced out of the resolved path already stored on the mutating fd.

Dispatch runs under a single global registry lock, so it is serialised
across the whole system rather than per hive or per source.

## 5.6.3.4 Depth

`MaxSubtreeWatchDepth` bounds how far below a watched key a subtree
watch is told about. The default is 0, meaning unlimited. A non-zero
value suppresses events whose path is longer than it, limiting both
noise and dispatch cost for a watch armed high in a tree.

The limit applies to watches held by userspace. LCS's own internal
watches are collected before the depth test and are not subject to it
(§5.10.4).

## 5.6.3.5 Transaction batches

Nothing inside an uncommitted transaction dispatches. At commit, and
only when the source reports success, the transaction's mutation log is
walked in operation order and the whole set of events is queued as one
batch, under a single hold of the registry lock, so no other operation
interleaves with it. An aborted, failed or timed-out transaction
dispatches nothing and its log is released.

A large transaction — a role installation writing thousands of values —
could otherwise fill a watcher's queue atomically. So the batch is
counted per watcher first: any watcher whose share exceeds
`MaxTransactionWatchEventBurst`, default 4096, receives exactly one
`OVERFLOW` and none of its individual events. That `OVERFLOW` is queued
ahead of the batch, so it arrives before whatever else that watcher is
still owed.

## 5.6.3.6 Recovery dispatch

Some changes alter effective state for keys whose fds nobody holds and
whose descendants the kernel is retaining no context for. Deleting a
layer, changing its precedence, enabling or disabling it, and restoring
a subtree from a stream are all of this kind. Computing an exact diff
would mean walking arbitrary parts of the tree through the source.

LCS does not attempt it. It increments the affected hive's generation
number and then queues a no-name `OVERFLOW` to every armed watch on the
affected source, which is the notification for those changes. The
watcher re-reads, and can compare the generation number it last saw
against the current one (§5.5.3) to tell whether it missed anything.

Two properties of this delivery are worth stating. It is object-
semantic like every other dispatch — it walks the watch map, resolves
no paths — and it bypasses the filter, because `OVERFLOW` always does.
It does not disarm anything.

The scope is the **source**, not the hive. A source backing several
hives delivers `OVERFLOW` to watches on all of them, including hives
the operation did not touch. The generation counters are maintained per
hive; the watch delivery is not.

For a restore, recovery is published only after the source commit
succeeds. A restore that fails or aborts before commit emits nothing.

## 5.6.3.7 Source restart

When a source disconnects, watches stay armed and nothing is delivered;
operations needing the source return `EIO`. `OVERFLOW` arrives on
**re-registration**, not on disconnect, which is the only ordering that
works: a watcher told to re-read needs the source to be there when it
does. Existing fds resume without re-opening, and no watcher has to
re-arm.

---

# 5.6.4 Queues and Overflow

_Peios / Advanced Peios / PKM / LCS / Watches_

> Each armed fd has its own bounded queue and delivery is best-effort — what a full queue does, and what a watcher does about it.

Each armed fd has its own event queue, bounded by
`NotificationQueueSize` — default 256 events, configurable between 16
and 65536 (§5.10.3).

Delivery is best-effort. A watcher that reads promptly sees every
change; one that falls behind is told that it has, rather than being
given a partial history it cannot distinguish from a complete one.

## 5.6.4.1 What happens when the queue is full

When an event arrives for a full queue and no `OVERFLOW` is present:

1. The oldest queued event is dropped.
2. An `OVERFLOW` record is queued in its place.
3. The event that triggered this is **discarded**, not queued.

Once an `OVERFLOW` is in the queue, subsequent events queue normally:
the oldest non-`OVERFLOW` event is dropped to make room and the new
event is added, so the single `OVERFLOW` is preserved and the queue
continues to carry the most recent history behind it.

A queue therefore holds at most one `OVERFLOW` at a time, and that is
an enforced invariant rather than a convention — an attempt to queue a
second is rejected as a bug.

## 5.6.4.2 What a watcher does about it

`OVERFLOW` means the record it has is incomplete. There is no way to
learn what was dropped, and no attempt is made to describe it. The
watcher re-reads the watched key, and its subtree if the watch is a
subtree watch, and continues from the state it finds. Events after the
`OVERFLOW` are complete again.

`REG_IOC_QUERY_KEY_INFO` reports a per-hive generation number
(§5.5.3) that makes this cheaper than it sounds: a watcher that
recorded the generation at its last full read can compare it with the
current one and skip the re-read entirely if nothing committed in
between.

## 5.6.4.3 Memory

There is no registry-specific global cap on watch memory, and none is
needed. A watch costs at most `NotificationQueueSize` queued events, a
watch requires an fd, and a process holds at most `RLIMIT_NOFILE` fds.
The product of the two is the bound, and it is enforced by machinery
that already exists.

The same reasoning covers open key state: per-fd overhead — GUID,
granted mask, ancestor chain, watch state — multiplied by the fd limit.

LCS's own internal watches are outside this. They are delivered
synchronously through a kernel callback rather than queued, so the
queue limit does not apply to them (§5.10.4).

---

# 5.7.1 Scope and Lifetime

_Peios / Advanced Peios / PKM / LCS / Transactions_

> Grouping registry operations so they commit together or not at all — binding, states, timeout, and sources that do not support them.

A transaction groups registry operations so that they commit together
or not at all. Installing a role writes service definitions, defaults
and registry entries; a transaction is what makes that one event rather
than a sequence of partially-applied ones.

`reg_begin_transaction` takes no arguments and returns a transaction
fd. It contacts no source: it allocates an id, creates an anonymous
inode, starts the lifetime timer, and returns. A transaction begins in
the state `REG_TXN_ACTIVE_UNBOUND`.

## 5.7.1.1 Binding

A transaction binds to a source on its first **mutating** operation.
There are seven: writing a value, deleting a value entry, setting or
removing a blanket tombstone, creating a key, deleting a key's path
entry, hiding a key, and changing a Security Descriptor.

Reads never bind. A read passed an unbound transaction fd is sent to
the source with a transaction id of zero and behaves as an ordinary
non-transactional read.

Once bound, every operation on that fd must target the same hive.
One that does not fails with `EXDEV`, and it fails in the kernel,
before anything reaches a source — a source never sees a cross-hive
operation inside a transaction. Cross-source atomicity would require
two-phase commit and is not supported.

Binding identity is the pair of source and hive root GUID carried on
the transaction object, which identifies the hive exactly.

## 5.7.1.2 Sources that do not support transactions

Because `reg_begin_transaction` chooses no source, it cannot fail for
lack of transaction support. The failure surfaces on the operation that
would have bound: LCS sends `RSI_BEGIN_TRANSACTION`, the source answers
`RSI_TXN_NOT_SUPPORTED`, and the operation returns `ENOTSUP`. The
transaction stays `ACTIVE_UNBOUND`, its source binding untouched, and
the caller can use the fd against a different hive.

There is no advance check of whether a source supports transactions.
The answer comes from the source, on the attempt.

If the source is Down before the first bind, the binding operation
fails `EIO` under the ordinary rules and the transaction likewise
remains unbound. If a source goes Down after binding, the transaction
becomes `REG_TXN_SOURCE_DOWN` (§5.8.5).

## 5.7.1.3 States

| State | Value | Meaning |
|---|---|---|
| `REG_TXN_ACTIVE_UNBOUND` | 0 | Active, no source chosen. |
| `REG_TXN_ACTIVE_BOUND` | 1 | Active, bound to a source. |
| `REG_TXN_COMMITTED` | 2 | Commit completed. |
| `REG_TXN_ABORTED` | 3 | Explicitly or implicitly aborted. |
| `REG_TXN_TIMED_OUT` | 4 | The lifetime timer fired. |
| `REG_TXN_SOURCE_DOWN` | 5 | The bound source went Down. |

`REG_IOC_TXN_STATUS` reports the state and a `terminal_errno`: 0 for
`COMMITTED`, `EINVAL` for `ABORTED`, `ETIMEDOUT` for `TIMED_OUT`, `EIO`
for `SOURCE_DOWN`, and 0 while active.

For `COMMITTED`, `terminal_errno` is not the errno that a further
operation would return. A committed transaction reports 0, but using
its fd again returns `EINVAL`.

## 5.7.1.4 Reaching a terminal state

- **Commit.** `REG_IOC_COMMIT` on the transaction fd. The source
  applies everything atomically, watch events fire, and the object
  becomes `COMMITTED`. Later use of the fd returns `EINVAL`. The fd
  should be closed.
- **Explicit abort.** `close()` without committing. The source is told
  to discard, and the object becomes `ABORTED` during release. No
  events.
- **Implicit abort.** Process death closes the fd, which aborts it.
  There are no orphaned transactions.
- **Timeout.** The lifetime timer fires. See below.

The object stays addressable after reaching a terminal state, until the
fd is closed. That is what makes `REG_IOC_TXN_STATUS` useful.

Transaction fds are pollable. Any terminal transition wakes poll
waiters with `POLLERR | POLLHUP`. A caller that needs a race-free
reason for the wakeup queries the status.

## 5.7.1.5 Timeout

The lifetime timer starts when the fd is created — not at the first
operation — and runs for `TransactionTimeoutMs`, default 30 seconds.

When it fires, the object becomes `TIMED_OUT`, poll waiters are woken,
and further use of the fd returns `ETIMEDOUT`. If the transaction was
bound and no commit is already in flight, `RSI_ABORT_TRANSACTION` is
sent to the source. The fd is not removed from the caller's table;
`close()` still releases it normally.

The timeout is not a convenience. Sources serialise writers — loregd
does so through SQLite's WAL — so a stalled transaction blocks every
other write to that source. The timer is what bounds the starvation
window, and `MaxBoundTransactionsPerSource` (default 16) is what stops
colluding processes from extending it indefinitely by binding fresh
transactions at each timeout boundary. An operation that would bind
past that cap returns `EBUSY`.

The cap is tested before the source is contacted, but after the
transaction's mutation-log entry has been allocated; that entry is
freed on the way out.

## 5.7.1.6 Constraints

- **No nesting.** Transactions are flat: no savepoints, no
  sub-transactions. The transaction fd accepts only `REG_IOC_COMMIT`
  and `REG_IOC_TXN_STATUS`; every other ioctl is `ENOTTY`.
- **One transaction per fd.** A process may hold many transaction fds
  at once, but each is exactly one transaction.
- **Reads are permitted.** A transaction is not write-only, which is
  what makes verify-then-write possible.

---

# 5.7.2 Isolation and the Mutation Log

_Peios / Advanced Peios / PKM / LCS / Transactions_

> Read-your-own-writes inside a transaction, implemented by the source rather than LCS — the mutation log, sequence numbers and conflicts.

## 5.7.2.1 Read-your-own-writes

Within a bound transaction, reads see the transaction's own uncommitted
writes. LCS does not implement this; the source does. A read tagged
with the transaction id is executed by the source inside its open
transaction, which naturally includes the pending writes. No
uncommitted registry data is cached in the kernel for the purpose of
resolving reads.

Externally, only committed state is visible. A transaction's writes are
invisible to other threads and processes until commit.

## 5.7.2.2 The mutation log

LCS does keep something: a per-transaction **mutation log**, holding
what the kernel needs to know after a successful commit and cannot ask
the source for afterwards. Each accepted mutating operation records the
affected key, value or layer name, its assigned sequence number, the
ancestor chain for watch dispatch, and enough context to compute
effective-state changes.

The log is not the source of truth for reads and it is not a rollback
journal — the source's own transaction state is authoritative for
uncommitted data. The log exists so that when the source says "yes",
LCS can produce the hive generation updates and the watch events that
correspond to what was just committed.

It is bounded at 4096 entries. That bound is a compile-time constant
rather than one of the self-configuration parameters. Exceeding it
fails the operation with `ENOMEM`.

An operation whose log entry cannot be allocated fails **before it is
sent to the source**. That ordering is deliberate: an operation the
source applied but the kernel cannot account for is exactly the state
the log exists to prevent.

The log is released, with no events emitted, on explicit abort, on a
lifetime timeout that fires before a commit is dispatched, on
source-down cancellation, on a late commit error, and on source
teardown while a post-dispatch commit response is still retained. It is
**retained** across a post-dispatch commit timeout, because the source
may still answer.

## 5.7.2.3 Sequence numbers

A transactional mutation is assigned its sequence number when it is
accepted into the transaction, not at commit. This preserves the order
in which the caller performed the operations, which is what layer
tiebreaking and watch ordering need.

If the transaction aborts, those numbers are simply never used. Gaps in
the sequence space are normal and mean nothing (§5.3.7).

## 5.7.2.4 Layer precedence coherency

Layer metadata lives in the registry, so a transaction can write to it —
and that raises the question of whether a precedence change takes
effect inside the transaction that made it. It does not.

Resolution always uses the published layer cache, which is refreshed
only at commit. Reading the `Precedence` value back inside the
transaction shows the new number, because that is an ordinary
read-your-own-writes read from the source. Resolving any *other* value
in the same transaction still uses the old precedence order. The two
are consistent with each other and with the rule that a transaction's
effects become real at commit.

The refresh is deferred to commit, dropped on abort, and performed
before `REG_IOC_COMMIT` returns on success.

## 5.7.2.5 Conflicts

Transactions are atomic. They are not conflict-detecting.

If two transactions write the same value, both commits succeed and the
one that committed second wins, because its write carries the higher
sequence number. There is no read set, no version check, and no
per-key validation anywhere in the commit path.

Serialising concurrent writers is the source's responsibility, not
LCS's. The RSI requires only that commits are atomic, that writes
within a transaction are ordered, and that concurrent commits are
serialised.

Conditional writes are the mechanism for the cases where losing an
update matters. `REG_IOC_SET_VALUE` takes an `expected_sequence`, the
source verifies it atomically against the layer's own entry, and a
mismatch returns `EAGAIN` (§5.5.3). That is a per-operation check, not
a transaction-level one, and it is deliberately scoped to a single
layer: a higher-precedence layer overriding a value is not a conflict,
it is the layer system working.

---

# 5.7.3 Commit and Failure

_Peios / Advanced Peios / PKM / LCS / Transactions_

> What a commit triggers in order, the failures that leave a transaction open, and what happens when the watch events cannot be delivered.

`REG_IOC_COMMIT` marks the transaction as having a commit in flight and
sends `RSI_COMMIT_TRANSACTION`. What happens next depends on the answer.

## 5.7.3.1 Success

The source's `RSI_OK` triggers, in order: the layer metadata cache
refresh for any layer names the transaction touched, the hive
generation increment, orphan tracking for keys that lost their last
path entry, and the watch event batch derived from the mutation log.
The object then becomes `COMMITTED`, the log is released, poll waiters
are woken, and the ioctl returns 0.

The hive generation is incremented once per committed transaction per
affected hive, however many operations the transaction contained.

## 5.7.3.2 Failure that leaves the transaction open

A source that cannot take the write lock answers `RSI_TXN_BUSY`, which
becomes `EBUSY`; a synchronous commit failure becomes `EIO`. In both
cases the transaction stays `ACTIVE_BOUND`:

- the mutation log is retained;
- no watch events are emitted;
- poll waiters are **not** woken as though the transaction had become
  terminal.

The in-flight marker is cleared, so the caller may simply retry
`REG_IOC_COMMIT`, or close the fd to abort. Nothing has been lost.

## 5.7.3.3 Timeout after dispatch

If the request timeout expires after the commit was dispatched, the
caller receives `ETIMEDOUT` and the object becomes `TIMED_OUT`, but the
mutation log is kept and the request record stays in the source's
in-flight table. The source may still answer.

`ETIMEDOUT` on a commit means *may or may not have committed*. A caller
that needs certainty checks state before retrying.

A late `RSI_OK` applies the full set of kernel-side effects from the
retained log — the same generation updates and the same watch events an
on-time commit would have produced. Watchers may therefore observe the
effects of a transaction whose caller was told it timed out. A late
error releases the log with no effects.

The transaction object does **not** move to `COMMITTED` when a late
success arrives. It stays `TIMED_OUT`, so a caller that queries
`REG_IOC_TXN_STATUS` afterwards is told `TIMED_OUT` with a
`terminal_errno` of `ETIMEDOUT`, even though the writes are durable and
the watch events have gone out. The state reflects what the caller was
told, not what the source did.

## 5.7.3.4 When the watch events cannot be derived

Two of the post-commit steps query the source. Working out which keys
were orphaned by a key deletion needs a lookup that can only be made
after the commit, and expanding a blanket tombstone into per-value
events needs the value set.

If that derivation cannot complete exactly, LCS does not reinterpret a
successful commit as a failed one and does not emit a partial set of
events. It delivers `OVERFLOW` to the affected watchers instead,
releases the retained replay state, and reports the commit as
successful, which it was. A late response arriving afterwards does not
resurrect the individual events once overflow recovery has been
chosen.

The carve-out is narrower than it might appear. It covers the orphan
lookup and the watch batch. The other two post-commit steps — publishing
the layer metadata cache and recording the hive generation — are state
updates rather than event derivation, and a failure in either returns
`EIO` and marks the source Down.

## 5.7.3.5 Abort

Aborting generates no events, ever, and releases the log. The source is
told to roll back with `RSI_ABORT_TRANSACTION` if the transaction was
bound.

Process death is the same path: closing the fd aborts.

---

# 5.8.1 The Source Model

_Peios / Advanced Peios / PKM / LCS / Sources_

> A source is a userspace process holding registry data and answering LCS over RSI — the trust boundary, and why loregd is not special.

A source is a userspace process that holds registry data and answers
LCS's questions about it over the Registry Source Interface. LCS is
source-agnostic: it does not know or care how a source stores anything.

The division is the sixth semantic rule (§5.1). A source stores path
entries, key records, value entries and blanket tombstones, and returns
**all** of them on request. It does not evaluate access, resolve layers,
dispatch watches, interpret paths beyond the parent and child names it
is given, or see the identity of any caller. LCS does all of that.

A source may back several hives; a hive is backed by exactly one source
(§5.2.1).

## 5.8.1.1 The interface is specified elsewhere

The RSI — its channel, framing, operations, error vocabulary, and the
obligations binding on a conforming source — is a normative
specification, because the userspace side is a role a third party can
implement. It is a chapter of PSPK, and it is where the wire format
lives.

This section covers the kernel's side: how LCS admits a source, how it
dispatches requests and accounts for them, what it refuses to believe,
and what it does when a source dies or answers late.

## 5.8.1.2 The trust boundary

Sources are in the TCB. LCS trusts the data a source returns —
descriptors, values, symlink targets, key metadata — because it has no
independent copy of any of it.

A compromised source therefore has complete control over access
decisions for its hives. It can return a permissive descriptor for any
key and AccessCheck will grant access that should have been denied. LCS
validates that responses are structurally correct, but it cannot detect
data that is well-formed and wrong: a descriptor granting Everyone
`KEY_ALL_ACCESS` on a sensitive key is a perfectly valid descriptor.

Three consequences are worth naming specifically.

**Layer table poisoning.** A compromised source backing `Machine\` can
fabricate the `Precedence` and `Enabled` values under
`Machine\System\Registry\Layers\`, and so control which layer wins
every resolution contest system-wide. The `SeTcbPrivilege` check at
write time does nothing about a fabricated read.

**Layer authorization bypass.** The same source can return permissive
descriptors for layer metadata keys, granting any process write access
to any layer (§5.3.4).

**`SeRestorePrivilege` implies descriptor control.** Restore replaces a
subtree including every descriptor in it, so granting
`SeRestorePrivilege` effectively grants `WRITE_DAC` and `WRITE_OWNER`
over everything in reach of a restore. Operators should understand it
that way.

The mitigations are operational rather than architectural: sources run
with tightly scoped privileges, protected by descriptors on their
service definitions, and managed by peinit with Process Integrity
Protection where available. LCS emits audit events for every source
data validation failure. A harder guarantee — checksumming descriptors
LCS computed during inheritance and verifying them on retrieval — is
possible but not implemented.

## 5.8.1.3 loregd is not special

loregd is the first source and the one that provides `Machine\` and
`Users\` at boot, which puts it on the critical path to a running
system. Nothing in LCS knows that. Its details are its own manual's
business; what LCS requires of it is exactly what it requires of any
source.

---

# 5.8.2 Registration and Slots

_Peios / Advanced Peios / PKM / LCS / Sources_

> Reaching LCS through /dev/pkm_registry — what registration validates, how source slots work, and resuming one that went Down.

A source reaches LCS through `/dev/pkm_registry`. The device's `open()`
handler checks the calling thread's effective token for an **enabled**
`SeTcbPrivilege` and returns `EPERM` without it, so an unprivileged
process cannot obtain an fd to the device at all.

Having opened it, the source issues `REG_SRC_REGISTER`, naming the
hives it backs, the root GUID of each, the highest sequence number it
has persisted, and per-hive flags and scope GUID. On success it enters
the request loop: `read()` for requests, `write()` for responses.

## 5.8.2.1 What registration validates

- Every hive name is valid and is not the reserved `CurrentUser`
  (§5.2.1).
- No route identity — the folded name paired with its scope — collides
  with one held by another **Active** source.
- Private hive collisions are scoped: the same name in different scopes
  is fine (§5.2.2).
- A hive's root GUID is not nil, and the root GUIDs within one request
  are distinct from each other.
- A hive without `RSI_HIVE_PRIVATE` carries no scope GUID, and no
  unknown flag bits are set.
- The hive count is non-zero and within `MaxHivesPerSource` (64), and
  the source count is within `MaxRegisteredSources` (32). Either is
  `ENOSPC`.
- The reported maximum sequence can be advanced past without
  overflowing 64 bits, or registration fails `EOVERFLOW` and the source
  is never made Active (§5.3.7).

Root GUIDs are checked for uniqueness within one request, and the
already-registered state is checked for consistency, but an incoming
request's root GUIDs are not compared against those of existing slots.

## 5.8.2.2 Source slots

Successful registration creates a **source slot**: the kernel object
owning one connection and the hive set registered on it.

Each registered hive has a stable identity — its folded name, its
visibility, its scope GUID for a private hive, and its root GUID.

**Down slots keep their identities reserved.** A crash or an fd close
marks the slot Down; it does not unregister anything and does not
retire any hive identity. Slots are never freed, and collision checks
see Down slots as well as Active ones.

Status is a property of the slot, not of an individual hive. A source's
hives go Down together.

## 5.8.2.3 Resuming a Down slot

A new process may take over a Down slot if it holds `SeTcbPrivilege`
and registers **exactly the same hive set**: the same folded name,
visibility, scope GUID and root GUID for every hive, and the same
number of them. Partial resume is rejected.

The distinction between the failures matters. A request whose only
mismatch is stale identity data — a different root GUID for an
otherwise-matching hive — fails `ESTALE`. Other partial or malformed
resume attempts fail `EINVAL`. `EEXIST` is reserved for a collision
with an **Active** slot; it never comes from a Down-slot collision.
When both an Active collision and a stale Down slot apply, `EEXIST`
wins.

New hives cannot be added by mutating a Down slot during a resume — the
hive set has to match exactly, so a superset simply fails, and a new
hive needs a new slot. There is no implicit retirement of a Down slot;
retiring one would need an explicit administrative operation, and none
exists.

A replacement source is authenticated by `SeTcbPrivilege`, not by
process identity. Nothing records a pid, and nothing could usefully:
process identity does not survive a crash and restart.

## 5.8.2.4 Coming back

On a successful resume the slot becomes Active, its restart generation
advances, and LCS replays any layer deletions that were pending, then
delivers `OVERFLOW` to every armed watch on that source (§5.6.3).

Existing key fds resume working without being reopened. Each carries
the restart generation it last saw; when it notices a change it
re-reads its key and continues. If the key's GUID no longer exists in
the restarted source — the database was restored from an older backup,
say — that first operation returns `ENOENT` and the fd is marked
orphaned.

---

# 5.8.3 Request Dispatch

_Peios / Advanced Peios / PKM / LCS / Sources_

> The RSI is multiplexed — request ids and matching, the single deadline covering three waits, and requests with no caller.

The RSI is multiplexed. LCS sends concurrent requests tagged with
request ids and matches responses back to the kernel threads waiting on
them. A source may process requests in any order.

Request ids are allocated per connection, strictly increasing, and
**never reused** while the connection lives — including after a
timeout. The id is allocated inside the queue lock, after the in-flight
limit has been checked, so a caller queued waiting for a slot does not
hold one yet.

`MaxConcurrentRSIRequests`, default 256, bounds how many requests may
be dispatched and awaiting a response at once. It is back-pressure for
a slow source.

## 5.8.3.1 One deadline covers three waits

`RequestTimeoutMs`, default 30 seconds, is measured from the point a
kernel operation first attempts to **reserve an in-flight slot**, after
local validation and access checks have already passed. One deadline is
computed there and reused for all three legs: waiting for a slot,
waiting for the source to read the queued request, and waiting for the
response.

If the deadline expires before a slot is reserved, the caller gets
`ETIMEDOUT` and **no request is sent**. If it expires after dispatch,
the caller gets `ETIMEDOUT` and late-response handling applies
(§5.8.5).

The deadline is checked before admission is attempted, not only after a
contention round is lost. It used to be the latter, which meant a request
finding a slot immediately free was dispatched with an already-expired
deadline and timed out in the wait leg instead — so the rule above held
only under contention, the one case where it is hardest to observe.

## 5.8.3.2 Timed-out requests keep their slot

For every dispatched request LCS keeps a **request record** until a
matching response is processed or the connection is torn down. The
record holds the request id, the operation code, the transaction id,
the key GUID it concerns, the runtime limits in force, and any retained
effect the kernel will need if the source later reports success.

When the deadline expires after dispatch, LCS detaches the waiting
caller from the record and returns `ETIMEDOUT`. **The record stays in
the in-flight table and keeps counting against
`MaxConcurrentRSIRequests`.** A timeout does not free a slot; only a
response or a teardown does.

A source that accumulates timed-out requests can therefore exhaust its
own in-flight slots until it answers or disconnects. That is the
intended shape: a source that stops answering stops being usable.

## 5.8.3.3 Requests with no caller

LCS dispatches some requests with nobody waiting: `RSI_DROP_KEY` after
the last fd to an orphaned key closes (§5.2.9), and
`RSI_ABORT_TRANSACTION` cleaning up source transaction state.

Such a record occupies an in-flight slot and is retained like any
other, and its response is validated normally and released normally.
But it is **not** a late response, and the retained-effect recovery
rules do not apply to it merely because nobody is waiting.

The kernel tracks the difference explicitly, with two booleans: whether
a waiter is attached now, and whether one was ever attached. A record
that never had a caller is not a timed-out request; only one whose
caller was detached after its deadline is.

This is load-bearing rather than pedantic. `RSI_DROP_KEY` is a mutating
operation, so without the distinction a perfectly ordinary answer to a
caller-less cleanup request would look like a mutation the kernel could
not account for, and would tear the source down.

## 5.8.3.4 The channel

`/dev/pkm_registry` is message-oriented. One `read()` returns exactly
one complete request; a buffer too small for the next one returns
`EMSGSIZE` **without consuming it**. An empty queue blocks, or returns
`EAGAIN` under `O_NONBLOCK`, or returns 0 if the fd is closing. One
`write()` submits exactly one complete response, and its length must
equal the response's own `total_len` exactly.

`poll` reports the fd readable when a 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 has not yet registered
reports nothing at all.

Any rejected `write()` — short, over-long, an unknown or duplicate
request id, an operation code that does not match the request, a
response for another connection — returns `EINVAL` **and tears the
connection down**. A source that cannot speak the protocol correctly is
not one whose other answers are worth believing.

---

# 5.8.4 Validation

_Peios / Advanced Peios / PKM / LCS / Sources_

> LCS validates every response before using it — malformed data against malformed protocol, and the asymmetric extensibility between them.

LCS validates every response before using it, and the failures split
into two categories with very different consequences.

## 5.8.4.1 Malformed data

The RSI message is structurally valid but its content is not: a
descriptor that will not parse, a value type that does not exist, a
sequence number that cannot be real, a metadata block that does not
cover the GUIDs it should.

- The request returns `EIO` to its caller.
- An `LCS_SOURCE_VALIDATION_FAILURE` audit event is emitted, naming the
  source slot and — where known — the hive, the request id, the
  operation code, the key GUID, and which of the twelve validation
  classes applies (§5.4.4).
- **The source stays alive.** Corruption may be localised, and one bad
  key is not a reason to take a hive offline.

What is checked, by category:

- **Security Descriptors**, from lookups and from layer metadata
  refreshes, must parse and must satisfy the ACE mask rules of §5.4.2.
  A malformed layer metadata descriptor additionally leaves the
  previous known-good one cached (§5.3.3).
- **Names** — layer names, key and child names, value names — must be
  valid under the ordinary rules for their kind.
- **Sequence numbers** must be below the next number LCS would
  allocate, and must not duplicate at the same precedence in a way that
  would decide a winner (§5.3.6).
- **Payload shape** — an otherwise-matched response whose
  operation-specific payload is the wrong shape, carries trailing
  bytes, or encodes a path target invalidly.
- **Metadata closure** — a lookup or enumeration whose per-GUID
  metadata block has missing, duplicate, unreferenced or nil entries.
  A HIDDEN entry must carry an all-zero GUID and contributes no
  metadata.
- **Value payloads** — invalid types, a tombstone carrying data, data
  above `MaxValueSize`.
- **Orphan lists** — a nil or duplicated GUID in an `RSI_DELETE_LAYER`
  response.
- **Status codes** outside the defined vocabulary.

## 5.8.4.2 Malformed protocol

The message itself is structurally invalid: bad framing, a truncated
response, an unknown request id, a duplicate response, an operation
code that does not match the request.

This is treated as a source crash. The connection is torn down, the
in-flight table is destroyed with every waiter completed `EIO`, the
slot is marked Down, its hives become unavailable, and bound
transactions enter `SOURCE_DOWN`.

There is one case where malformed *data* also takes the source down:
when the caller had already timed out and the operation was a commit or
a replayable mutation. At that point LCS cannot establish whether a
mutation was applied, and it cannot account for one it cannot describe
(§5.8.5).

## 5.8.4.3 Asymmetric extensibility

Requests and responses do not extend the same way.

A **request** may carry trailing fields a source does not recognise; a
source skips them using `total_len`. That is how a new optional field
is added without an RSI version bump.

A **response** may not. LCS rejects any trailing bytes in a response
payload as malformed data. Forward compatibility on the response side
comes from new operations, not from extending existing payloads.

---

# 5.8.5 Failure and Late Responses

_Peios / Advanced Peios / PKM / LCS / Sources_

> What happens when a source dies, and the late-response problem — a late error, a late successful read, and a late successful write.

## 5.8.5.1 When a source dies

The connection closes unexpectedly, and:

1. The slot is marked Down and its hives become unavailable.
2. Every pending request fails `EIO`, including ones queued but not yet
   delivered.
3. **Open key fds stay valid.** An fd holds a GUID and a granted mask,
   and neither depends on the source. Operations needing a round trip
   return `EIO` until the source comes back.
4. Bound transactions enter `REG_TXN_SOURCE_DOWN`, their poll waiters
   are woken with `POLLERR | POLLHUP`, their mutation logs are
   released, and further use of those fds returns `EIO`.
5. **Watches stay armed.** Watch state is kernel-side and does not
   depend on the source at all. Nothing is delivered during the window,
   and `OVERFLOW` arrives on re-registration rather than on disconnect
   (§5.6.3).

Coming back is covered in §5.8.2.

## 5.8.5.2 The late response problem

A caller that times out after its request was dispatched is gone, but
the request is not. The source may still apply the operation and answer
minutes later, and by then there is nobody to return a value to — while
the *kernel* still has work to do, because a mutation that succeeded
has to produce its generation increment and its watch events.

This is the most intricate part of LCS and the part with the most ways
to be subtly wrong.

A late response is validated exactly like an on-time one. The rules
that follow apply **only** to a request whose caller was detached after
its deadline, never to one that never had a caller (§5.8.3).

### 5.8.5.2.1 A late error

The record is released. No watch events, no generation change, nothing.

### 5.8.5.2.2 A late successful read

Validated, then discarded. There is nobody to give it to and nothing
about it changes kernel state.

### 5.8.5.2.3 A late successful mutation

The kernel-side effects that correspond to the mutation are applied
from the retained record: the hive generation increment, watch
dispatch, the layer metadata cache refresh, and — for a commit — the
transaction's batch effects and orphan tracking.

Which mutations can actually be replayed is narrower than the set of
operations that count as mutating. A replayable effect is recorded for
`RSI_SET_VALUE` and `RSI_WRITE_KEY`, and only for non-transactional
calls. For the other mutating operations — creating, hiding or deleting
a path entry, creating or dropping a key, deleting a value entry,
setting a blanket tombstone, deleting a layer — no effect was retained,
so a late success is a mutation the kernel cannot account for. LCS
tears the source down and returns `EIO` rather than silently ignoring
it.

That is the conservative direction, and deliberately so: the
alternative is a committed change with no watch event and a stale
generation number, which nothing downstream could detect.

### 5.8.5.2.4 A late successful transaction operation

A late `RSI_BEGIN_TRANSACTION` has created transaction state in the
source that nobody will ever use, so LCS enqueues an
`RSI_ABORT_TRANSACTION` for that id.

A late `RSI_ABORT_TRANSACTION` or `RSI_FLUSH` releases the record with
no effects.

A late `RSI_COMMIT_TRANSACTION` is a mutating response and applies the
retained commit effects — the same generation update and the same watch
events an on-time commit would have produced. Watchers may therefore
observe the effects of a transaction whose caller was told it timed
out (§5.7.3).

### 5.8.5.2.5 A malformed late response

The ordinary malformed-data and malformed-protocol rules apply. But if
LCS cannot safely process the kernel-side effects of a
possibly-applied mutation because the request metadata it needs is
missing or invalid, it tears the source down and marks it Down rather
than ignoring the response. A malformed late commit response takes the
source down for the same reason.

## 5.8.5.3 What a caller should conclude

`ETIMEDOUT` means *may or may not have completed*. A caller that needs
certainty reads state back before retrying. That applies to every
operation and, especially, to a transaction commit.

## 5.8.5.4 Fd lifecycle

Key fds and transaction fds are ordinary file descriptors, subject to
`RLIMIT_NOFILE`. There is no registry-specific fd accounting and no
registry-specific leak protection.

Process exit closes them through normal kernel cleanup: key fds
released and their watches removed, transactions aborted.

## 5.8.5.5 Memory

Registry kernel memory needs no global cap, because everything is
bounded by limits that already exist.

- **Watch queues:** `NotificationQueueSize` per queue, times
  `RLIMIT_NOFILE` queues per process.
- **Open key state:** per-fd overhead — GUID, granted mask, ancestor
  chain, watch state — times the same fd limit.
- **Layer table:** bounded by `MaxTotalLayers`, and per-value
  resolution cost by `MaxLayersPerValue`.
- **In-flight requests:** bounded per source by
  `MaxConcurrentRSIRequests`, and separately by the number of threads
  blocked on registry syscalls.

---

# 5.9.1 The Stream

_Peios / Advanced Peios / PKM / LCS / Backup and Restore_

> The streamable binary representation of a key and everything beneath it with full layer fidelity — what the design buys, and how it is versioned.

The registry backup format is a streamable binary representation of a
key and everything beneath it, with full layer fidelity. It is used by
`REG_IOC_BACKUP` and `REG_IOC_RESTORE`, by first-boot seeding, by
disaster recovery and by offline migration.

It is an **LCS-level** format. Sources never see it: LCS serialises
from source data on the way out and deserialises into RSI operations on
the way in.

Because a third party writes and reads these streams directly — that is
what migration and recovery mean — the format is a normative
specification rather than a description. It is a chapter of PSPK, and
every byte layout, record type, ordering rule and validation
requirement lives there. This section covers what LCS does with it.

## 5.9.1.1 What the design buys

**Streamable.** It is written to an arbitrary fd — a file, a pipe, a
socket — with no seeking, in a single pass, and read back the same way.

**Full layer fidelity.** Every path entry, value, tombstone and blanket
tombstone is stored with its layer tag, so restoring reconstructs the
layered state rather than a flattened snapshot of it.

**Depth-first pre-order.** A parent always appears before its children,
so a restore can create keys top-down without buffering a tree.

**Descriptors inline.** Each key record carries its own descriptor with
no deduplication. Redundancy is external compression's problem; piping
through zstd handles it.

**Self-verifying.** A trailer carries a record count and a SHA-256 over
everything before it, so truncation and corruption are detected.

## 5.9.1.2 Versioning

The header carries a format version and a **minimum reader version**. A
writer that used only older features sets a lower minimum, letting
older readers restore the stream; a reader that finds a minimum above
its own supported version rejects the stream outright, before touching
anything.

Both are 21 in the current implementation, and the reader supports 21.

Unknown record types are skipped when the minimum reader version allows
it, and they still count toward the record count and the checksum. A
writer that adds a record type a restore genuinely needs must raise the
minimum reader version, so that an older reader refuses the stream
rather than restoring an incomplete one.

Extension is by new **record types** only. Existing record payloads
must be consumed exactly; trailing bytes inside a known record are an
error. This is the opposite of the RSI's request convention (§5.8.4),
and the difference is deliberate: a stream is replayed into mutations
long after it was written, and a field silently ignored there is data
silently lost.

---

# 5.9.2 Backup

_Peios / Advanced Peios / PKM / LCS / Backup and Restore_

> Writing a subtree to a stream under SeBackupPrivilege with no per-key access check — the snapshot, the audit, and what is written.

`REG_IOC_BACKUP` exports the key on the fd and its entire subtree to
another fd.

It requires `SeBackupPrivilege` and performs **no per-key AccessCheck**
whatsoever. The privilege is the whole authorisation, which is why the
operation is audited unconditionally (§5.4.4). The output fd must be
writable, or `EBADF`.

## 5.9.2.1 The snapshot

Before reading anything, LCS opens a **read-only** source transaction —
`RSI_BEGIN_TRANSACTION` with mode `RSI_TXN_READ_ONLY` — so that the
whole export is a point-in-time snapshot. Concurrent mutations do not
appear part-way through the stream.

That transaction is **released with `RSI_ABORT_TRANSACTION` and never
committed**. There is no commit call anywhere in the backup path; a
read-only transaction has nothing to commit.

A source that does not support read-only snapshots answers
`RSI_TXN_NOT_SUPPORTED` and the backup fails `ENOTSUP`. A source
already holding `MaxReadOnlyTransactionsPerSource` snapshots (default
16) yields `EBUSY` — which bounds snapshot-holding without treating
backups as write-lock holders, since they are not.

Backing up an orphaned key is `ENOENT`: it is no longer a reachable
subtree root (§5.2.9).

## 5.9.2.2 Audit

`LCS_BACKUP_START` is emitted **before any subtree data is read**, and
if it cannot be emitted the backup returns `EIO` and does not start.
`LCS_BACKUP_COMPLETE` is emitted afterwards, carrying the result, and a
failure to emit it cannot change a result that has already happened.

## 5.9.2.3 What is written

The stream is a header, the layer manifest, then each key in
depth-first pre-order with its path entries, values and blanket
tombstones, then the trailer. The exact shapes are in the PSPK chapter.

Two things about the exporter are worth stating here, because they
constrain a reader more tightly than the format does.

The exporter writes **no GUID-bearing path entries for the backup
root**. The root's section contains only its hidden entries. A reader
tolerates and skips them if some other writer produces them, but this
one does not emit them, because on restore the target key's existing
name is authoritative and they would be discarded anyway.

Hidden entries belong to the **parent's** section, not to a section of
their own — a hidden entry has no key to have a section for. A hidden
entry masking a name where no key exists in any layer is still valid;
it expresses "this layer hides this name" whether or not anything is
there to hide.

The layer manifest is written from the live layer table, and it is a
manifest: it records what the layers looked like at backup time so that
a restore can validate the stream against them. It is not a backup of
the layer definitions. A layer definition is backed up only when
`Machine\System\Registry\Layers\<Name>\` is itself inside the subtree
being exported, in which case it is ordinary key and value data like
anything else.

---

# 5.9.3 Restore

_Peios / Advanced Peios / PKM / LCS / Backup and Restore_

> Restore is a replace rather than a merge — one transaction, the surviving target key, the order of operations, and the precedence gate.

`REG_IOC_RESTORE` **replaces** the key on the fd and its entire subtree
from a stream. It is not a merge: the target's contents and descendants
are torn down before the stream's contents are written.

It requires `SeRestorePrivilege` and, like backup, performs no per-key
AccessCheck and is audited unconditionally. The input fd must be
readable, or `EBADF`, and restoring onto an orphaned key is `ENOENT`.

Because restore rewrites every descriptor in the subtree,
`SeRestorePrivilege` effectively confers `WRITE_DAC` and `WRITE_OWNER`
over everything within reach of one (§5.8.1).

## 5.9.3.1 One transaction

The entire restore — teardown and rebuild together — is wrapped in a
single read-write source transaction. A source that answers
`RSI_TXN_NOT_SUPPORTED` for `RSI_TXN_READ_WRITE` cannot be a restore
target: restore requires atomicity and there is no partial-restore
mode. Every failure path aborts the transaction, so a failed restore
rolls back the teardown as well.

## 5.9.3.2 The target key survives

The stream's header names a root GUID, and that GUID is **remapped** to
the already-open target key everywhere it appears — in parent
references, in child references, in value key references — before
anything is validated or dispatched.

The target key **object** is not replaced. Its GUID, parent, name,
volatile flag and symlink flag remain what they were; they are never
taken from the stream. What the stream's root record supplies is the
mutable part: the Security Descriptor and the last write time, written
to the target with `RSI_WRITE_KEY` inside the transaction.

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, is `EINVAL`.

Descendants keep their backup GUIDs. Those are written into the target
verbatim.

## 5.9.3.3 Order of operations

1. Validate the whole stream, including the trailer's record count and
   checksum, and retain every replayable record.
2. Read the layer manifest and apply the precedence gate (below).
3. Verify the root record and its immutable flags against the live
   target.
4. Tear down the target's contents and descendants — path entries,
   values, blanket tombstones, and descendant key records — inside the
   transaction.
5. Write the root's mutable fields, then replay its section's values
   and blanket tombstones.
6. For each non-root key in stream order: create it, write its last
   write time, then replay its path entries, values and blanket
   tombstones.
7. Commit.

The stream is read from the fd exactly once, sequentially. Nothing
seeks, so a pipe is a valid input.

Validating the whole stream first is stronger than the format requires:
a checksum failure aborts before any source mutation rather than after
some. The cost is memory rather than seeking — the replayable records
are retained in kernel memory across the teardown.

## 5.9.3.4 The precedence gate

Before any key record is written, LCS checks the layer manifest. If any
declared layer has a precedence above 0, **or** any existing cached
layer table entry with the same folded identity does, and the caller
does not hold `SeTcbPrivilege`, the restore aborts with `EPERM` before
a single byte reaches the source.

And if the stream contains ordinary key and value records for
`Machine\System\Registry\Layers\<Name>\`, writes that create or raise
persisted metadata above precedence 0 hit the ordinary inline
`SeTcbPrivilege` check as well (§5.3.4).

Both exist so that `SeRestorePrivilege` cannot be used to smuggle a
Group Policy-tier layer past the defence in depth that guards
precedence.

## 5.9.3.5 Layers in a restored stream

Manifest records create, update, delete, enable, disable and authorise
nothing. If restored entries reference a layer that is not in the
current 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 (§5.3.6). If the metadata subtree *is* included, it is
restored through the ordinary path, and those records — not the
manifest — are what define the layer.

## 5.9.3.6 Sequence remapping

Backup sequence numbers preserve the backup's internal ordering, but a
restore is a new mutation and its entries must outrank everything
already present. So the numbers are remapped rather than written
through.

Before dispatching the first layer-qualified record, LCS takes the
global sequence-allocation gate and records the current next sequence
as the offset. Every restored record is then written with
`offset + backup_sequence`, which preserves relative order while
placing the whole set above pre-restore state.

The gate is held until the restore reaches a terminal state. Other
sequence-allocating mutations wait; reads are not blocked. A restore
with no layer-qualified records at all never takes it.

If a remapped value would overflow, the restore fails `EOVERFLOW` — and
it fails at validation, before teardown, rather than part-way through.
The valid remapped range stops just below `U64_MAX`, which is never
handed out (§5.3.7).

When the restore reaches any terminal state — commit, abort, failure or
cancellation — LCS advances the global counter past the highest number
it dispatched, and **does not roll that back**. Sequence numbers a
failed restore dispatched become unused gaps, exactly like those of any
other failed write.

## 5.9.3.7 GUID collisions

A GUID that appears twice in one stream, other than the root, is
`EINVAL`, and so is a non-root GUID equal to the target root's.

A non-root GUID that already exists **outside** the subtree being
replaced is `EEXIST` — but it is discovered by the source rejecting the
create during replay, not by a check beforehand. LCS has no index of
which GUIDs exist elsewhere, so the collision surfaces mid-restore, and
the transaction rolls back.

The parent of every path entry, after remapping, must be either the
restore root or a key record already processed earlier in the stream.
That check *is* made up front, and it is what stops a crafted backup
injecting path entries into arbitrary parts of the existing namespace
outside the subtree being replaced.

## 5.9.3.8 Watches

A restore is an arbitrary subtree replacement and LCS retains no exact
before-and-after diff for it. On a successful commit it publishes the
affected hive's generation increment and dispatches a no-name
`OVERFLOW` to the armed watches on that source (§5.6.3). A restore that
fails or aborts before commit emits nothing.

---

# 5.10.1 The Bootstrap Problem

_Peios / Advanced Peios / PKM / LCS / Bootstrap and Self-Configuration_

> The registry has to configure itself — the three circular dependencies, and the three rules that break them.

The registry is the configuration store for the whole system, and it
has to configure itself. Three circular dependencies have to be broken
before it can serve anyone.

**peinit needs the registry to start services, but the registry source
is a service.** Service definitions live in the registry, and the
process that would serve them has not started.

**LCS needs operational parameters from the registry, but the registry
needs LCS.** Timeouts, caps and limits live under
`Machine\System\Registry\`, in a hive LCS itself routes.

**A fresh install has no data at all.** The source's database is empty;
there is nothing to read.

Three rules break all three.

## 5.10.1.1 Rule 1: compiled-in defaults

Every operational parameter has a compiled-in default, and LCS runs on
those defaults from the moment PKM initialises. There is no "waiting
for configuration" state, no flag, and no wait queue: the limits
structure is statically initialised at load, the source char device is
registered without consulting configuration, and the first attempt to
read configuration happens on a workqueue after a source has already
registered.

Before any source registers, the routing table is empty, so every
operation that names a hive returns `ENOENT`. That is the only sense in
which LCS is not yet useful, and it is not a distinct state — it is
just an empty table.

## 5.10.1.2 Rule 2: the base layer exists unconditionally

The base layer is a static constant in the kernel: name `base`,
precedence 0, enabled. It is handed out whenever the dynamic layer
table is empty and is always written first into every layer snapshot.

A source that registers with an entirely empty database is fully
functional, because the one layer that writes need is not in the
database. Persisted metadata under
`Machine\System\Registry\Layers\base\` may exist and may decorate the
base layer, but it is not required and cannot contradict it (§5.3.2).

## 5.10.1.3 Rule 3: hot-swap, not restart

When configuration becomes available, LCS reads it, validates it, and
swaps the values in place. It does not restart, re-initialise, or
block. The whole transition from compiled-in defaults to registry-backed
configuration is driven by the internal self-watch (§5.10.4).

## 5.10.1.4 Source dependencies are the source's problem

LCS neither knows nor cares what a source depends on. A source that
needs the root filesystem, a SYSTEM token, or a particular kernel
feature arranges that for itself. LCS's only requirement is that the
process can open `/dev/pkm_registry` and speak RSI.

---

# 5.10.2 The Boot Sequence

_Peios / Advanced Peios / PKM / LCS / Bootstrap and Self-Configuration_

> What happens from PKM initialisation to a registered source, on a normal boot and on a first boot, and the contract it establishes.

## 5.10.2.1 Normal boot

```
Kernel boots
  → PKM initialises; LCS runs on compiled-in defaults
  → /dev/pkm_registry registered
  → the base layer exists in memory

A source registers — loregd, started by peinit
  → opens /dev/pkm_registry (SeTcbPrivilege checked at open)
  → REG_SRC_REGISTER: hive names, root GUIDs, max persisted sequence
  → LCS initialises or advances next_sequence to max + 1

Bootstrap refresh is queued
  → resolve Machine\System\Registry     → read and hot-swap parameters
  → resolve Machine\System\Registry\Layers → populate the layer table
  → arm internal subtree watches
```

The bootstrap refresh runs on a workqueue after `REG_SRC_REGISTER`
returns, so registration never blocks on configuration and a source
that registers can start answering immediately.

The refresh is triggered by the arrival of a **global hive named
`Machine`**. That name is matched case-insensitively in the kernel and
is one of the two hive names LCS knows about; the other is `Users`, the
target of `CurrentUser\` rewriting (§5.2.1). Neither is a routing
decision — routing is entirely dynamic — but the claim that the kernel
holds no hive names at all would not be true.

## 5.10.2.2 First boot

An empty source has no `Machine\System\Registry` to read.

```
Source detects an empty database on first startup
  → generates root GUIDs for the hives it backs
  → creates root key records with their default SDs
  → persists them, then registers with LCS

LCS resolves Machine\System\Registry → does not exist
  → compiled-in defaults retained
  → subtree watch armed on the Machine\ hive root instead

peinit notices the registry is empty and restores the seed backup.
That is peinit's decision, not LCS's.

Seed restore populates Machine\ through REG_IOC_RESTORE
  → the fallback subtree watch fires
  → LCS re-runs the bootstrap refresh: resolves the specific GUIDs,
    re-arms targeted watches, validates and hot-swaps the seed values,
    and re-reads layer metadata
```

Hive root Security Descriptors are set by the source, not by LCS. LCS
holds no template for them and enforces whatever the source stores
(§5.4.3).

## 5.10.2.3 Watch arming in practice

The spec-level description above says the fallback watch is armed
*instead of* the targeted ones. What the kernel actually arms is a
**mixed** set: targeted watches on whichever of the two roots exist,
*plus* the `Machine\` root fallback. The fallback is a superset rather
than a substitute, and it stays armed until a refresh finds both roots
present.

A third internal watch is armed by the same sequence, on
`Machine\System\KMES`, which is not LCS configuration at all — it is
how KMES picks up its own parameters from the registry LCS serves.

## 5.10.2.4 The bootstrap contract

Four properties hold throughout and are relied on elsewhere:

1. **LCS is always operational with compiled-in defaults.** It accepts
   syscalls from the moment PKM initialises.
2. **The base layer requires no persisted state.**
3. **Hot-swap is the only configuration transition.** LCS never blocks,
   restarts, or re-initialises.
4. **Source dependencies are the source's concern.**

---

# 5.10.3 Operational Parameters

_Peios / Advanced Peios / PKM / LCS / Bootstrap and Self-Configuration_

> The nineteen parameters LCS reads from the registry, how they are validated, and what hot-swapping one does to in-flight operations.

LCS reads nineteen parameters from `Machine\System\Registry\`. All are
`REG_DWORD`. Each has a compiled-in default and a valid range, and LCS
runs on the defaults until the registry says otherwise.

| Value | Default | Range | Bounds |
|---|---:|---|---|
| `RequestTimeoutMs` | 30000 | 1000–600000 | A source round trip (§5.8.3). |
| `TransactionTimeoutMs` | 30000 | 1000–600000 | The lifetime of an open transaction (§5.7.1). |
| `NotificationQueueSize` | 256 | 16–65536 | Queued events per watcher before overflow (§5.6.4). |
| `SymlinkDepthLimit` | 16 | 1–64 | Symlink resolution depth (§5.2.4). |
| `MaxValueSize` | 1048576 | 4096–67108864 | One value's data, in bytes. |
| `MaxKeyDepth` | 512 | 32–4096 | Key hierarchy nesting. |
| `MaxPathComponentLength` | 255 | 64–1024 | One key, value or layer name, in UTF-8 bytes. |
| `MaxTotalPathLength` | 16383 | 1024–65535 | A whole path, in UTF-8 bytes. |
| `MaxLayersPerValue` | 128 | 1–1024 | Layers writing to one `(key, value name)` (§5.3.1). |
| `MaxBoundTransactionsPerSource` | 16 | 1–256 | Concurrently bound transactions per source (§5.7.1). |
| `MaxReadOnlyTransactionsPerSource` | 16 | 1–256 | Concurrent backup snapshots per source (§5.9.2). |
| `MaxTotalLayers` | 1024 | 16–65536 | Distinct layers in the in-memory table (§5.3.1). |
| `MaxRegisteredSources` | 32 | 1–256 | Concurrently registered sources. |
| `MaxHivesPerSource` | 64 | 1–1024 | Hives one source may register. |
| `MaxConcurrentRSIRequests` | 256 | 8–4096 | In-flight RSI requests per source (§5.8.3). |
| `MaxScopeGUIDsPerToken` | 8 | 1–256 | Private hive scope GUIDs on a token (§5.2.2). |
| `MaxPrivateLayersPerToken` | 16 | 1–256 | Private layer names on a token (§5.3.5). |
| `MaxSubtreeWatchDepth` | 0 | 0–4096 | Subtree watch depth; 0 is unlimited (§5.6.3). |
| `MaxTransactionWatchEventBurst` | 4096 | 256–65536 | Watch events per watcher from one commit (§5.6.3). |

Unknown values under this key are ignored. There are exactly nineteen
parameters and no undocumented ones; a separate set under
`Machine\System\KMES\` belongs to KMES.

Two limits are not among them. The **transaction mutation log** is
capped at 4096 entries by a compile-time constant (§5.7.2), and hard
ceilings on total path length and key depth exist independently of
configuration — set equal to the range maxima above, so they never
conflict.

## 5.10.3.1 Where a configured value does not fully bind

Three of the nineteen do not do everything their range suggests.

`MaxTotalLayers` may be configured up to 65536, but the in-memory layer
table is a fixed array sized at compile time for 1023 dynamic layers
plus the base layer. A value above 1024 validates and publishes, and
then layer creation fails `ENOSPC` at 1023 regardless. Values below
1024 bind correctly.

`MaxPrivateLayersPerToken` is described as an attachment-time limit but
is not enforced at attachment. KACS applies its own hard cap of 256 and
LCS applies the configured value later, at use, with `E2BIG` (§5.3.5).
`MaxScopeGUIDsPerToken` behaves the same way.

`SymlinkDepthLimit` is honoured on most of the walk but two call sites
use the compiled-in default of 16 instead of the configured value.

## 5.10.3.2 Validation

A value is checked against its range when it is read.

- **Valid** — hot-swapped into the in-memory configuration and used by
  new operations.
- **Invalid** — out of range, the wrong type, or missing — the value is
  **ignored** and the previously active one is kept: the compiled-in
  default or the last known-good. An `LCS_SELF_CONFIG_INVALID` audit
  event is emitted naming the parameter, what was wrong, and the value
  being retained (§5.4.4).

**Values are never clamped or silently corrected.** There is no
`min`/`max` on any configuration path. A write to the registry
succeeds, because the source does not enforce kernel semantics, and LCS
simply refuses to use it. The registry shows what was written; the
audit log shows what LCS is running on.

Because "missing" is invalid, a first boot before seed restore emits
nineteen of these events per refresh.

## 5.10.3.3 Hot-swap and in-flight operations

Configuration is published as a whole structure under a seqlock, and a
reader takes a complete copy of it. A syscall entry point snapshots it
once and threads that snapshot through the operation, so in-flight work
uses the values that were current when it started and new work uses the
updated ones.

That is the rule, and mostly the practice. Some deeper paths take a
second snapshot part-way through, and a few call sites read a single
live value rather than a snapshot — `reg_begin_transaction`'s timeout,
the bound-transaction cap, and the in-flight request cap among them.
For those, a hot-swap can be observed mid-operation.

## 5.10.3.4 Security

`Machine\System\Registry\` inherits the `Machine` hive root descriptor
— SYSTEM and Administrators with `KEY_ALL_ACCESS`, Authenticated Users
with `KEY_READ` — so an unprivileged process cannot change any of this.

Domain policy at a higher-precedence layer defends against a
compromised local administrator, which is the reason `SeTcbPrivilege`
guards precedence above 0 (§5.3.4).

---

# 5.10.4 The Self-Watch

_Peios / Advanced Peios / PKM / LCS / Bootstrap and Self-Configuration_

> LCS watches its own configuration through the same machinery userspace uses — what it drives, how it is armed, and why the callback is not the atomicity boundary.

LCS watches its own configuration and layer metadata subtrees, and it
does so through the same machinery userspace uses — but not through the
same interface.

An internal watch is an entry in the same watch map, taking a reference
on the same subtree watch set, distinguished only by a kind marker. It
has **no fd, no granted access mask and no filter**. Events reach it
through a kernel callback rather than being queued for a `read()`, and
it is therefore not subject to `NotificationQueueSize`. It is also not
subject to `MaxSubtreeWatchDepth`, nor to the transaction burst
suppressor: internal collection happens before either test.

Because there is no filter, deliverability is decided per target rather
than by a bitmask. Each internal target admits only the event types it
cares about: value events on the watched key itself for the
configuration subtrees, and subkey events at depth 0 or value and
descriptor events at depth 1 for the layer metadata subtree.

## 5.10.4.1 What it drives

**Self-configuration.** A change under `Machine\System\Registry\`
triggers a re-read and validation of the parameters (§5.10.3).

**The layer table.** A change under `Machine\System\Registry\Layers\`
marks the affected layer names dirty and drives a bounded refresh of
their precedence, enabled state, owner and cached descriptor.

**Layer lifecycle.** `SUBKEY_CREATED` and `SUBKEY_DELETED` under
`Layers\` add and remove layers, except for `base`, which is ignored
(§5.3.2).

**KMES configuration.** A third internal watch, on
`Machine\System\KMES\`, exists for KMES's own parameters. It is not
LCS configuration, but the registry is where it lives and this is the
mechanism that notices it change.

## 5.10.4.2 The callback is not the atomicity boundary

For layer metadata, internal delivery identifies which layer names are
dirty. It does not itself publish anything, and it must not: publishing
a layer means publishing its table entry, metadata key GUID and cached
descriptor together (§5.3.3), and a callback that published a partial
entry would create a window in which a layer exists and nobody can be
authorised against it.

LCS also does not perform source round trips while holding the
watch-map or layer-table publication locks. The refresh runs outside
them, after the mutating operation commits and before the syscall
returns.

## 5.10.4.3 Arming

At bootstrap, LCS resolves the GUIDs for `Machine\System\Registry\` and
`Machine\System\Registry\Layers\` through `RSI_LOOKUP` and arms
targeted subtree watches. If either does not exist — first boot, empty
database — it arms a subtree watch on the `Machine\` hive root instead,
so that seed restore creating the subtree is noticed. That fallback
event re-enters the whole bootstrap refresh, which resolves the
specific GUIDs and arms the targeted watches.

In practice the kernel arms both: targeted watches for whichever roots
exist, **plus** the `Machine\` root fallback, until a refresh finds
everything present. It is a superset of what is needed rather than a
substitute for it.

## 5.10.4.4 Bootstrap interaction

1. A source registers. LCS reads `Machine\System\Registry\*`; the keys
   do not exist; compiled-in defaults are retained.
2. Seed restore populates them. The subtree watch fires, LCS validates
   and hot-swaps to the seed values.
3. Subsequent administrative changes fire the watch again, and LCS
   validates and hot-swaps, or rejects with an audit event.

At no point is there a state in which LCS is waiting for
configuration.

---

# Appendix 5.A LCS ABI Reference

_Peios / Advanced Peios / PKM / LCS_

> Every LCS syscall number, ioctl, structure layout and constant, generated from the uapi headers and measured by compilation.

Every name, value, offset and size in this appendix is generated from
`pkm/uapi/pkm/lcs.h` by `pkm/tools/gen-lcs-abi.py`, with ioctl
encodings and struct layouts measured by compiling a probe against the
real header. Regenerate it whenever the ABI changes; do not edit it by
hand. The names here are the ones a program actually compiles against.

What a compiler cannot measure -- which properties belong with their
operations rather than here, and the kernel configuration -- is in the
notes appendix, §5.B, which this generator does not touch.

## 5.A.1 Syscall numbers

Signatures are read from the `SYSCALL_DEFINE` sites in `pkm/lcs/`.

| Number | Constant | Signature |
|---|---|---|
| 1100 | `SYS_REG_OPEN_KEY` | `reg_open_key(int parent_fd, const char __user *path, u32 desired_access, u32 flags)` |
| 1101 | `SYS_REG_CREATE_KEY` | `reg_create_key(const struct reg_create_key_args __user *args)` |
| 1102 | `SYS_REG_BEGIN_TRANSACTION` | `reg_begin_transaction(void)` |

## 5.A.2 Ioctls

The type byte is `'R'`. Ioctl number namespaces are per fd type, so
`REG_SRC_REGISTER` (number 0 on the source device) and
`REG_IOC_QUERY_VALUE` (number 0 on a key fd) do not collide: the
kernel dispatches on the fd's `file_operations`, not globally. The
encoded value is what `_IOC` produces from the direction, type byte,
number and argument size.

*Source device fd.*

| Ioctl | Number | Direction | Argument | Arg size | Encoded |
|---|---|---|---|---|---|
| `REG_SRC_REGISTER` | 0 | _IOW | `struct reg_src_register_args` | 24 | `0x40185200` |

*Key fd.*

| Ioctl | Number | Direction | Argument | Arg size | Encoded |
|---|---|---|---|---|---|
| `REG_IOC_QUERY_VALUE` | 0 | _IOWR | `struct reg_query_value_args` | 64 | `0xC0405200` |
| `REG_IOC_SET_VALUE` | 1 | _IOW | `struct reg_set_value_args` | 64 | `0x40405201` |
| `REG_IOC_DELETE_VALUE` | 2 | _IOW | `struct reg_delete_value_args` | 40 | `0x40285202` |
| `REG_IOC_BLANKET_TOMBSTONE` | 3 | _IOW | `struct reg_blanket_tombstone_args` | 24 | `0x40185203` |
| `REG_IOC_QUERY_VALUES_BATCH` | 4 | _IOWR | `struct reg_query_values_batch_args` | 24 | `0xC0185204` |
| `REG_IOC_ENUM_VALUES` | 5 | _IOWR | `struct reg_enum_value_args` | 40 | `0xC0285205` |
| `REG_IOC_ENUM_SUBKEYS` | 6 | _IOWR | `struct reg_enum_subkey_args` | 40 | `0xC0285206` |
| `REG_IOC_QUERY_KEY_INFO` | 7 | _IOWR | `struct reg_query_key_info_args` | 64 | `0xC0405207` |
| `REG_IOC_DELETE_KEY` | 8 | _IOW | `struct reg_delete_key_args` | 24 | `0x40185208` |
| `REG_IOC_HIDE_KEY` | 9 | _IOW | `struct reg_hide_key_args` | 24 | `0x40185209` |
| `REG_IOC_GET_SECURITY` | 10 | _IOWR | `struct reg_get_security_args` | 16 | `0xC010520A` |
| `REG_IOC_SET_SECURITY` | 11 | _IOW | `struct reg_set_security_args` | 24 | `0x4018520B` |
| `REG_IOC_NOTIFY` | 12 | _IOW | `struct reg_notify_args` | 8 | `0x4008520C` |
| `REG_IOC_FLUSH` | 13 | _IO | none | 0 | `0x0000520D` |
| `REG_IOC_BACKUP` | 14 | _IOW | `struct reg_backup_args` | 4 | `0x4004520E` |
| `REG_IOC_RESTORE` | 15 | _IOW | `struct reg_restore_args` | 4 | `0x4004520F` |

*Transaction fd.*

| Ioctl | Number | Direction | Argument | Arg size | Encoded |
|---|---|---|---|---|---|
| `REG_IOC_COMMIT` | 16 | _IO | none | 0 | `0x00005210` |
| `REG_IOC_TXN_STATUS` | 17 | _IOR | `struct reg_txn_status_args` | 8 | `0x80085211` |

## 5.A.3 Structure layouts

Offsets and sizes are measured, not declared. The header also defines
a `_SIZE` constant for each of these structures; the two agree by
construction, and a mismatch fails the build in `uapi/smoke_test.c`.

### 5.A.3.1 `struct reg_create_key_args`

Total size 48 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__s32` | `parent_fd` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `path_ptr` |
| 16 | 4 | `__u32` | `desired_access` |
| 20 | 4 | `__u32` | `flags` |
| 24 | 8 | `__u64` | `layer_ptr` |
| 32 | 4 | `__s32` | `txn_fd` |
| 36 | 4 | `__u32` | `_pad1` |
| 40 | 8 | `__u64` | `disposition_ptr` |

### 5.A.3.2 `struct reg_query_value_args`

Total size 64 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `name_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 4 | `__u32` | `type` |
| 20 | 4 | `__u32` | `data_len` |
| 24 | 4 | `__s32` | `txn_fd` |
| 28 | 4 | `__u32` | `layer_buf_len` |
| 32 | 8 | `__u64` | `data_ptr` |
| 40 | 8 | `__u64` | `sequence` |
| 48 | 4 | `__u32` | `layer_len` |
| 52 | 4 | `__u32` | `_pad1` |
| 56 | 8 | `__u64` | `layer_ptr` |

### 5.A.3.3 `struct reg_set_value_args`

Total size 64 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `name_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 4 | `__u32` | `type` |
| 20 | 4 | `__u32` | `data_len` |
| 24 | 8 | `__u64` | `data_ptr` |
| 32 | 4 | `__u32` | `layer_len` |
| 36 | 4 | `__u32` | `_pad1` |
| 40 | 8 | `__u64` | `layer_ptr` |
| 48 | 4 | `__s32` | `txn_fd` |
| 52 | 4 | `__u32` | `_pad2` |
| 56 | 8 | `__u64` | `expected_seq` |

### 5.A.3.4 `struct reg_delete_value_args`

Total size 40 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `name_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 4 | `__u32` | `layer_len` |
| 20 | 4 | `__u32` | `_pad1` |
| 24 | 8 | `__u64` | `layer_ptr` |
| 32 | 4 | `__s32` | `txn_fd` |
| 36 | 4 | `__u32` | `_pad2` |

### 5.A.3.5 `struct reg_blanket_tombstone_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `layer_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `layer_ptr` |
| 16 | 1 | `__u8` | `set` |
| 17 | 3 | `__u8``[3]` | `_pad1` |
| 20 | 4 | `__s32` | `txn_fd` |

### 5.A.3.6 `struct reg_query_values_batch_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `buf_len` |
| 4 | 4 | `__u32` | `count` |
| 8 | 8 | `__u64` | `buf_ptr` |
| 16 | 4 | `__s32` | `txn_fd` |
| 20 | 4 | `__u32` | `_pad` |

### 5.A.3.7 `struct reg_enum_value_args`

Total size 40 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `index` |
| 4 | 4 | `__u32` | `name_len` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 4 | `__u32` | `type` |
| 20 | 4 | `__u32` | `data_len` |
| 24 | 8 | `__u64` | `data_ptr` |
| 32 | 4 | `__s32` | `txn_fd` |
| 36 | 4 | `__u32` | `_pad` |

### 5.A.3.8 `struct reg_enum_subkey_args`

Total size 40 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `index` |
| 4 | 4 | `__u32` | `name_len` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 8 | `__u64` | `last_write_time` |
| 24 | 4 | `__u32` | `subkey_count` |
| 28 | 4 | `__u32` | `value_count` |
| 32 | 4 | `__s32` | `txn_fd` |
| 36 | 4 | `__u32` | `_pad` |

### 5.A.3.9 `struct reg_query_key_info_args`

Total size 64 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `name_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 8 | `__u64` | `last_write_time` |
| 24 | 4 | `__u32` | `subkey_count` |
| 28 | 4 | `__u32` | `value_count` |
| 32 | 4 | `__u32` | `max_subkey_name_len` |
| 36 | 4 | `__u32` | `max_value_name_len` |
| 40 | 4 | `__u32` | `max_value_data_size` |
| 44 | 4 | `__u32` | `sd_size` |
| 48 | 1 | `__u8` | `volatile_key` |
| 49 | 1 | `__u8` | `symlink` |
| 50 | 6 | `__u8``[6]` | `_pad1` |
| 56 | 8 | `__u64` | `hive_generation` |

### 5.A.3.10 `struct reg_delete_key_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `layer_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `layer_ptr` |
| 16 | 4 | `__s32` | `txn_fd` |
| 20 | 4 | `__u32` | `_pad1` |

### 5.A.3.11 `struct reg_hide_key_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `layer_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `layer_ptr` |
| 16 | 4 | `__s32` | `txn_fd` |
| 20 | 4 | `__u32` | `_pad1` |

### 5.A.3.12 `struct reg_get_security_args`

Total size 16 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `security_info` |
| 4 | 4 | `__u32` | `sd_len` |
| 8 | 8 | `__u64` | `sd_ptr` |

### 5.A.3.13 `struct reg_set_security_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `security_info` |
| 4 | 4 | `__u32` | `sd_len` |
| 8 | 8 | `__u64` | `sd_ptr` |
| 16 | 4 | `__s32` | `txn_fd` |
| 20 | 4 | `__u32` | `_pad` |

### 5.A.3.14 `struct reg_notify_args`

Total size 8 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `filter` |
| 4 | 1 | `__u8` | `subtree` |
| 5 | 3 | `__u8``[3]` | `_pad` |

### 5.A.3.15 `struct reg_backup_args`

Total size 4 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__s32` | `output_fd` |

### 5.A.3.16 `struct reg_restore_args`

Total size 4 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__s32` | `input_fd` |

### 5.A.3.17 `struct reg_txn_status_args`

Total size 8 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `state` |
| 4 | 4 | `__s32` | `terminal_errno` |

### 5.A.3.18 `struct reg_src_register_args`

Total size 24 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `hive_count` |
| 4 | 4 | `__u32` | `_pad` |
| 8 | 8 | `__u64` | `max_sequence` |
| 16 | 8 | `__u64` | `hives_ptr` |

### 5.A.3.19 `struct reg_src_hive_entry`

Total size 56 bytes.

| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 4 | `__u32` | `name_len` |
| 4 | 4 | `__u32` | `_pad0` |
| 8 | 8 | `__u64` | `name_ptr` |
| 16 | 16 | `__u8``[16]` | `root_guid` |
| 32 | 4 | `__u32` | `flags` |
| 36 | 4 | `__u32` | `_pad1` |
| 40 | 16 | `__u8``[16]` | `scope_guid` |

## 5.A.4 Constants

Grouped as the header groups them.

*Syscall and ioctl argument sizes.*

| Constant | Value |
|---|---|
| `REG_CREATE_KEY_ARGS_SIZE` | `48` |
| `REG_QUERY_VALUE_ARGS_SIZE` | `64` |
| `REG_SET_VALUE_ARGS_SIZE` | `64` |
| `REG_DELETE_VALUE_ARGS_SIZE` | `40` |
| `REG_BLANKET_TOMBSTONE_ARGS_SIZE` | `24` |
| `REG_QUERY_VALUES_BATCH_ARGS_SIZE` | `24` |
| `REG_ENUM_VALUE_ARGS_SIZE` | `40` |
| `REG_ENUM_SUBKEY_ARGS_SIZE` | `40` |
| `REG_QUERY_KEY_INFO_ARGS_SIZE` | `64` |
| `REG_DELETE_KEY_ARGS_SIZE` | `24` |
| `REG_HIDE_KEY_ARGS_SIZE` | `24` |
| `REG_GET_SECURITY_ARGS_SIZE` | `16` |
| `REG_SET_SECURITY_ARGS_SIZE` | `24` |
| `REG_NOTIFY_ARGS_SIZE` | `8` |
| `REG_BACKUP_ARGS_SIZE` | `4` |
| `REG_RESTORE_ARGS_SIZE` | `4` |
| `REG_TXN_STATUS_ARGS_SIZE` | `8` |
| `REG_SRC_REGISTER_ARGS_SIZE` | `24` |
| `REG_SRC_HIVE_ENTRY_SIZE` | `56` |

*_IOWR, not _IOR: the kernel reads the caller's name_len and name_ptr out of the argument struct before it writes the result back, so the argument crosses in both directions. It was declared _IOR, which put the wrong direction bits in the encoded number -- and since the kernel dispatches on the whole encoded value, correcting it is a wire break, not a relabelling.*

| Constant | Value |
|---|---|
| `REG_IOC_QUERY_KEY_INFO` | `0xC0405207` |
| `REG_IOC_DELETE_KEY` | `0x40185208` |
| `REG_IOC_HIDE_KEY` | `0x40185209` |
| `REG_IOC_GET_SECURITY` | `0xC010520A` |
| `REG_IOC_SET_SECURITY` | `0x4018520B` |
| `REG_IOC_NOTIFY` | `0x4008520C` |
| `REG_IOC_FLUSH` | `0x0000520D` |
| `REG_IOC_BACKUP` | `0x4004520E` |
| `REG_IOC_RESTORE` | `0x4004520F` |

*Transaction state codes.*

| Constant | Value |
|---|---|
| `REG_TXN_ACTIVE_UNBOUND` | `0` |
| `REG_TXN_ACTIVE_BOUND` | `1` |
| `REG_TXN_COMMITTED` | `2` |
| `REG_TXN_ABORTED` | `3` |
| `REG_TXN_TIMED_OUT` | `4` |
| `REG_TXN_SOURCE_DOWN` | `5` |

*Syscall flags and dispositions.*

| Constant | Value |
|---|---|
| `REG_OPEN_LINK` | `0x01` |
| `REG_OPTION_VOLATILE` | `0x01` |
| `REG_OPTION_CREATE_LINK` | `0x02` |
| `REG_CREATED_NEW` | `1` |
| `REG_OPENED_EXISTING` | `2` |

*Registry key access rights.*

| Constant | Value |
|---|---|
| `KEY_QUERY_VALUE` | `0x00000001` |
| `KEY_SET_VALUE` | `0x00000002` |
| `KEY_CREATE_SUB_KEY` | `0x00000004` |
| `KEY_ENUMERATE_SUB_KEYS` | `0x00000008` |
| `KEY_NOTIFY` | `0x00000010` |
| `KEY_CREATE_LINK` | `0x00000020` |
| `DELETE` | `0x00010000` |
| `READ_CONTROL` | `0x00020000` |
| `WRITE_DAC` | `0x00040000` |
| `WRITE_OWNER` | `0x00080000` |
| `ACCESS_SYSTEM_SECURITY` | `0x01000000` |
| `MAXIMUM_ALLOWED` | `0x02000000` |
| `GENERIC_ALL` | `0x10000000` |
| `GENERIC_EXECUTE` | `0x20000000` |
| `GENERIC_WRITE` | `0x40000000` |
| `GENERIC_READ` | `0x80000000` |
| `KEY_READ` | `0x00020019` |
| `KEY_WRITE` | `0x00020006` |
| `KEY_ALL_ACCESS` | `0x000F003F` |
| `REG_VALID_DESIRED_ACCESS_MASK` | `0xF30F003F` |
| `REG_VALID_MAPPED_ACCESS_MASK` | `0x010F003F` |
| `REG_VALID_ACE_ACCESS_MASK` | `0xF10F003F` |

*Security information flags for REG_IOC_GET_SECURITY / SET_SECURITY.*

| Constant | Value |
|---|---|
| `OWNER_SECURITY_INFORMATION` | `0x00000001` |
| `GROUP_SECURITY_INFORMATION` | `0x00000002` |
| `DACL_SECURITY_INFORMATION` | `0x00000004` |
| `SACL_SECURITY_INFORMATION` | `0x00000008` |
| `REG_VALID_SECURITY_INFORMATION` | `0x0000000F` |

*Registry value types.*

| Constant | Value |
|---|---|
| `REG_NONE` | `0` |
| `REG_SZ` | `1` |
| `REG_EXPAND_SZ` | `2` |
| `REG_BINARY` | `3` |
| `REG_DWORD` | `4` |
| `REG_DWORD_BIG_ENDIAN` | `5` |
| `REG_LINK` | `6` |
| `REG_MULTI_SZ` | `7` |
| `REG_RESOURCE_LIST` | `8` |
| `REG_FULL_RESOURCE_DESCRIPTOR` | `9` |
| `REG_RESOURCE_REQUIREMENTS_LIST` | `10` |
| `REG_QWORD` | `11` |
| `REG_TOMBSTONE` | `0xFFFF` |

*Watch event types and filters.*

| Constant | Value |
|---|---|
| `REG_WATCH_VALUE_SET` | `1` |
| `REG_WATCH_VALUE_DELETED` | `2` |
| `REG_WATCH_SUBKEY_CREATED` | `3` |
| `REG_WATCH_SUBKEY_DELETED` | `4` |
| `REG_WATCH_SD_CHANGED` | `5` |
| `REG_WATCH_KEY_DELETED` | `6` |
| `REG_WATCH_OVERFLOW` | `7` |

*Watch event raw byte layout.*

| Constant | Value |
|---|---|
| `REG_WATCH_EVENT_TOTAL_LEN_OFFSET` | `0` |
| `REG_WATCH_EVENT_TYPE_OFFSET` | `4` |
| `REG_WATCH_EVENT_NAME_LEN_OFFSET` | `6` |
| `REG_WATCH_EVENT_NAME_OFFSET` | `8` |
| `REG_WATCH_EVENT_MIN_SIZE` | `8` |
| `REG_WATCH_SUBTREE_PATH_DEPTH_REL_OFFSET` | `0` |
| `REG_WATCH_SUBTREE_PATH_DEPTH_SIZE` | `2` |
| `REG_WATCH_SUBTREE_PATH_COMPONENTS_REL_OFFSET` | `2` |
| `REG_WATCH_PATH_COMPONENT_LEN_SIZE` | `2` |
| `REG_NOTIFY_VALUE` | `0x01` |
| `REG_NOTIFY_SUBKEY` | `0x02` |
| `REG_NOTIFY_SD` | `0x04` |
| `REG_NOTIFY_ALL` | `0x07` |

*RSI common wire layout.*

| Constant | Value |
|---|---|
| `RSI_REQUEST_TOTAL_LEN_OFFSET` | `0` |
| `RSI_REQUEST_ID_OFFSET` | `4` |
| `RSI_REQUEST_OP_CODE_OFFSET` | `12` |
| `RSI_REQUEST_TXN_ID_OFFSET` | `14` |
| `RSI_REQUEST_HEADER_SIZE` | `22` |
| `RSI_RESPONSE_TOTAL_LEN_OFFSET` | `0` |
| `RSI_RESPONSE_ID_OFFSET` | `4` |
| `RSI_RESPONSE_OP_CODE_OFFSET` | `12` |
| `RSI_RESPONSE_HEADER_SIZE` | `14` |
| `RSI_RESPONSE_STATUS_OFFSET` | `14` |
| `RSI_STATUS_SIZE` | `4` |
| `RSI_MIN_RESPONSE_SIZE` | `18` |
| `RSI_LENGTH_PREFIX_SIZE` | `4` |
| `RSI_GUID_SIZE` | `16` |
| `RSI_RESPONSE_BIT` | `0x8000` |

*RSI op codes and response op codes.*

| Constant | Value |
|---|---|
| `RSI_LOOKUP` | `0x0001` |
| `RSI_CREATE_ENTRY` | `0x0002` |
| `RSI_HIDE_ENTRY` | `0x0003` |
| `RSI_DELETE_ENTRY` | `0x0004` |
| `RSI_ENUM_CHILDREN` | `0x0005` |
| `RSI_CREATE_KEY` | `0x0010` |
| `RSI_READ_KEY` | `0x0011` |
| `RSI_WRITE_KEY` | `0x0012` |
| `RSI_DROP_KEY` | `0x0013` |
| `RSI_QUERY_VALUES` | `0x0020` |
| `RSI_SET_VALUE` | `0x0021` |
| `RSI_DELETE_VALUE_ENTRY` | `0x0022` |
| `RSI_SET_BLANKET_TOMBSTONE` | `0x0023` |
| `RSI_BEGIN_TRANSACTION` | `0x0030` |
| `RSI_COMMIT_TRANSACTION` | `0x0031` |
| `RSI_ABORT_TRANSACTION` | `0x0032` |
| `RSI_FLUSH` | `0x0040` |
| `RSI_DELETE_LAYER` | `0x0050` |
| `RSI_LOOKUP_RESPONSE` | `0x8001` |
| `RSI_CREATE_ENTRY_RESPONSE` | `0x8002` |
| `RSI_HIDE_ENTRY_RESPONSE` | `0x8003` |
| `RSI_DELETE_ENTRY_RESPONSE` | `0x8004` |
| `RSI_ENUM_CHILDREN_RESPONSE` | `0x8005` |
| `RSI_CREATE_KEY_RESPONSE` | `0x8010` |
| `RSI_READ_KEY_RESPONSE` | `0x8011` |
| `RSI_WRITE_KEY_RESPONSE` | `0x8012` |
| `RSI_DROP_KEY_RESPONSE` | `0x8013` |
| `RSI_QUERY_VALUES_RESPONSE` | `0x8020` |
| `RSI_SET_VALUE_RESPONSE` | `0x8021` |
| `RSI_DELETE_VALUE_ENTRY_RESPONSE` | `0x8022` |
| `RSI_SET_BLANKET_TOMBSTONE_RESPONSE` | `0x8023` |
| `RSI_BEGIN_TRANSACTION_RESPONSE` | `0x8030` |
| `RSI_COMMIT_TRANSACTION_RESPONSE` | `0x8031` |
| `RSI_ABORT_TRANSACTION_RESPONSE` | `0x8032` |
| `RSI_FLUSH_RESPONSE` | `0x8040` |
| `RSI_DELETE_LAYER_RESPONSE` | `0x8050` |

*RSI status codes.*

| Constant | Value |
|---|---|
| `RSI_OK` | `0` |
| `RSI_NOT_FOUND` | `1` |
| `RSI_ALREADY_EXISTS` | `2` |
| `RSI_STORAGE_ERROR` | `3` |
| `RSI_NOT_EMPTY` | `4` |
| `RSI_TOO_LARGE` | `5` |
| `RSI_TXN_BUSY` | `6` |
| `RSI_INVALID` | `7` |
| `RSI_CAS_FAILED` | `8` |
| `RSI_TXN_NOT_SUPPORTED` | `9` |

*RSI path target types.*

| Constant | Value |
|---|---|
| `RSI_PATH_TARGET_GUID` | `0` |
| `RSI_PATH_TARGET_HIDDEN` | `1` |

*RSI_WRITE_KEY field mask bits.*

| Constant | Value |
|---|---|
| `RSI_WRITE_KEY_FIELD_SD` | `0x01` |
| `RSI_WRITE_KEY_FIELD_LAST_WRITE_TIME` | `0x02` |
| `RSI_WRITE_KEY_FIELD_KNOWN_MASK` | `0x00000003` |

*RSI transaction modes and source-registration flags.*

| Constant | Value |
|---|---|
| `RSI_TXN_READ_WRITE` | `0` |
| `RSI_TXN_READ_ONLY` | `1` |
| `RSI_HIVE_PRIVATE` | `0x01` |

*Backup record types and magic.*

| Constant | Value |
|---|---|
| `REG_BACKUP_HEADER` | `0x01` |
| `REG_BACKUP_LAYER` | `0x02` |
| `REG_BACKUP_KEY` | `0x03` |
| `REG_BACKUP_PATH_ENTRY` | `0x04` |
| `REG_BACKUP_VALUE` | `0x05` |
| `REG_BACKUP_BLANKET_TOMBSTONE` | `0x06` |
| `REG_BACKUP_TRAILER` | `0xFF` |
| `REG_BACKUP_MAGIC` | `"PEIOSREG"` |

## 5.A.5 Tracepoint diagnostic codes

From `uapi/pkm/trace.h`. These are a diagnostic contract for
ftrace, perf and eBPF consumers, letting a tool decode an `lcs:`
event's `reason`, `op` or `state` field without recompiling
against a specific kernel. No LCS syscall accepts or returns
them, and values are append-only.

lcs_rsi_request op — which of the 18 RSI dispatch verbs a source-side
request admission record describes. Emitted by lcs:lcs_rsi_request on
successful queue admission and on the admission error rungs; the rung is
read from `ret` (0 == enqueued, -EAGAIN == in-flight at limit /
backpressure, -EIO == source gone / fd closing, -EOVERFLOW == request-id
space exhausted, other == build reject). The same op enum tags the
round-trip begin marker (lcs_rsi_roundtrip). Never records a pathname,
key name, GUID, or frame bytes — only this op code, ids, counts and ret.

| Constant | Value | Notes |
|---|---|---|
| `LCS_OP_LOOKUP` | `0` | RSI_LOOKUP |
| `LCS_OP_READ_KEY` | `1` | RSI_READ_KEY |
| `LCS_OP_ENUM_CHILDREN` | `2` | RSI_ENUM_CHILDREN |
| `LCS_OP_QUERY_VALUES` | `3` | RSI_QUERY_VALUES |
| `LCS_OP_SET_VALUE` | `4` | RSI_SET_VALUE |
| `LCS_OP_DELETE_VALUE` | `5` | RSI_DELETE_VALUE_ENTRY |
| `LCS_OP_BLANKET_TOMBSTONE` | `6` | RSI_SET_BLANKET_TOMBSTONE |
| `LCS_OP_DROP_KEY` | `7` | RSI_DROP_KEY |
| `LCS_OP_CREATE_ENTRY` | `8` | RSI_CREATE_ENTRY |
| `LCS_OP_HIDE_ENTRY` | `9` | RSI_HIDE_ENTRY |
| `LCS_OP_DELETE_ENTRY` | `10` | RSI_DELETE_ENTRY |
| `LCS_OP_CREATE_KEY` | `11` | RSI_CREATE_KEY |
| `LCS_OP_WRITE_KEY` | `12` | RSI_WRITE_KEY |
| `LCS_OP_TXN_BEGIN` | `13` | RSI_BEGIN_TRANSACTION |
| `LCS_OP_TXN_COMMIT` | `14` | RSI_COMMIT_TRANSACTION |
| `LCS_OP_TXN_ABORT` | `15` | RSI_ABORT_TRANSACTION |
| `LCS_OP_FLUSH` | `16` | RSI_FLUSH |
| `LCS_OP_DELETE_LAYER` | `17` | RSI_DELETE_LAYER |

lcs_rsi_response reason — the outcome of accepting/validating a source's
RSI response frame, and the late-response effects that silently mark a
source DOWN. ACCEPTED is the clean path; DESYNC / OP_MISMATCH /
UNKNOWN_STATUS are the accept-time rejects that all surface as
-EINVAL/-EIO; MALFORMED_PAYLOAD is a per-op body validation reject; the
LATE_* codes mark a response whose deferred effect
(commit/mutation/begin bookkeeping) failed and took the source DOWN.
Verdict/outcome is also in `ret`. Never records name/GUID/frame bytes.

| Constant | Value | Notes |
|---|---|---|
| `LCS_RESP_ACCEPTED` | `0` | response matched an in-flight request |
| `LCS_RESP_DESYNC` | `1` | no matching delivered/unaccepted record |
| `LCS_RESP_OP_MISMATCH` | `2` | response op != request op \| RESPONSE_BIT |
| `LCS_RESP_UNKNOWN_STATUS` | `3` | rsi_status not a known status code |
| `LCS_RESP_MALFORMED_PAYLOAD` | `4` | per-op response body failed validation |
| `LCS_RESP_LATE_COMMIT_FAIL` | `5` | commit late-effect failed; source DOWN |
| `LCS_RESP_LATE_MUTATION_FAIL` | `6` | mutation late-effect failed; source DOWN |
| `LCS_RESP_LATE_BEGIN_FAIL` | `7` | begin-txn late-effect failed; source DOWN |

*lcs_source_fd reason — which source-fd lifecycle transition a record marks.*

OPEN is a fresh /dev/pkm_registry fd; the remaining codes are the entry
points that drive a source to the DOWN/closing state. `source_down_id`
is the source id that transitioned DOWN (0 if the call was a no-op). The
semantic *cause* of a late-effect-driven DOWN is carried by
lcs_rsi_response (LCS_RESP_LATE_*); here EXPLICIT/MARK_BY_ID are the
mechanical transitions. No pathname/SD bytes.

| Constant | Value | Notes |
|---|---|---|
| `LCS_SRC_OPEN` | `0` | new source fd issued (post-TCB check) |
| `LCS_SRC_RELEASE` | `1` | fd .release() teardown |
| `LCS_SRC_MALFORMED` | `2` | malformed protocol frame -> mark down |
| `LCS_SRC_EXPLICIT` | `3` | explicit mark-down of this fd |
| `LCS_SRC_MARK_BY_ID` | `4` | mark-down requested by source id |

lcs_in_flight reason — an in-flight RSI request table transition (kept
lean; insert on admission, delivered when handed to the source's read(),
release on response completion or teardown). `in_flight_count` is the
post-transition depth. Emitted by lcs:lcs_in_flight.

| Constant | Value | Notes |
|---|---|---|
| `LCS_IF_INSERT` | `0` | request inserted into in-flight table |
| `LCS_IF_DELIVERED` | `1` | request delivered to source read() |
| `LCS_IF_RELEASE` | `2` | request released from in-flight table |

*lcs_route op — which resolution the lcs:lcs_route event describes.*

| Constant | Value | Notes |
|---|---|---|
| `LCS_ROUTE_HIVE_NAME` | `0` | hive-name -> source/root resolution |
| `LCS_ROUTE_ABSOLUTE_PATH` | `1` | absolute-path -> source/root resolution |
| `LCS_ROUTE_SYMLINK_TARGET` | `2` | symlink-target -> source/root resolution |

*lcs_registration decision — the source registration path.*

NEW/RESUME_DOWN are publish verdicts; COPY is the input-copy stage;
REPLAY_FAIL/OVERFLOW_FAIL are resume post-publish -EIO paths that mark
the resumed source down. Emitted by lcs:lcs_source_register /
_registration_publish / _registration_copy.

| Constant | Value | Notes |
|---|---|---|
| `LCS_REG_NEW` | `0` | new source slot admitted |
| `LCS_REG_RESUME_DOWN` | `1` | down source slot resumed |
| `LCS_REG_COPY` | `2` | registration input copied from user |
| `LCS_REG_REPLAY_FAIL` | `3` | resume pending-delete replay failed (EIO) |
| `LCS_REG_OVERFLOW_FAIL` | `4` | resume overflow dispatch failed (EIO) |

*lcs_bootstrap stage — the phase of a bootstrap / self-config refresh.*

Emitted by lcs:lcs_bootstrap_refresh / _self_config_refresh /
_self_config_publish.

| Constant | Value | Notes |
|---|---|---|
| `LCS_BOOT_REGISTRY` | `0` | registry root discover phase |
| `LCS_BOOT_KMES` | `1` | kmes config root discover phase |
| `LCS_BOOT_LAYERS` | `2` | layer metadata root discover phase |
| `LCS_BOOT_SELF_WATCH` | `3` | self-watch arm phase |
| `LCS_BOOT_COMPLETE` | `4` | bootstrap refresh completed |
| `LCS_BOOT_SELF_CONFIG_REFRESH` | `5` | self-config refresh-from-key outcome |
| `LCS_BOOT_SELF_CONFIG_PARAM_INVALID` | `6` | self-config publish rejected a parameter |

lcs_runtime_limits field_id — which runtime-limit field a validate
reject names, or LCS_LIM_ALL for a successful whole-struct publish.
Emitted by lcs:lcs_limits_validate (-EINVAL, `value` offending) and
lcs:lcs_limits_publish.

| Constant | Value | Notes |
|---|---|---|
| `LCS_LIM_REQUEST_TIMEOUT_MS` | `0` |  |
| `LCS_LIM_TRANSACTION_TIMEOUT_MS` | `1` |  |
| `LCS_LIM_NOTIFICATION_QUEUE_SIZE` | `2` |  |
| `LCS_LIM_SYMLINK_DEPTH_LIMIT` | `3` |  |
| `LCS_LIM_MAX_VALUE_SIZE` | `4` |  |
| `LCS_LIM_MAX_KEY_DEPTH` | `5` |  |
| `LCS_LIM_MAX_PATH_COMPONENT_LENGTH` | `6` |  |
| `LCS_LIM_MAX_TOTAL_PATH_LENGTH` | `7` |  |
| `LCS_LIM_MAX_LAYERS_PER_VALUE` | `8` |  |
| `LCS_LIM_MAX_BOUND_TRANSACTIONS_PER_SOURCE` | `9` |  |
| `LCS_LIM_MAX_READ_ONLY_TRANSACTIONS_PER_SOURCE` | `10` |  |
| `LCS_LIM_MAX_TOTAL_LAYERS` | `11` |  |
| `LCS_LIM_MAX_REGISTERED_SOURCES` | `12` |  |
| `LCS_LIM_MAX_HIVES_PER_SOURCE` | `13` |  |
| `LCS_LIM_MAX_CONCURRENT_RSI_REQUESTS` | `14` |  |
| `LCS_LIM_MAX_SCOPE_GUIDS_PER_TOKEN` | `15` |  |
| `LCS_LIM_MAX_PRIVATE_LAYERS_PER_TOKEN` | `16` |  |
| `LCS_LIM_MAX_SUBTREE_WATCH_DEPTH` | `17` |  |
| `LCS_LIM_MAX_TRANSACTION_WATCH_EVENT_BURST` | `18` |  |
| `LCS_LIM_ALL` | `19` | whole-struct publish (success) |

*lcs_audit event_type_id — which LCS audit event a record describes.*

Emitted by lcs:lcs_audit_emit and lcs:lcs_audit_emit_failed.
`result_errno` carries the op-specific numeric. No SD or raw GUID bytes;
key GUID is a u64 hash.

| Constant | Value | Notes |
|---|---|---|
| `LCS_AUDIT_KEY_OPEN` | `0` | key-open SACL audit |
| `LCS_AUDIT_BACKUP_START` | `1` |  |
| `LCS_AUDIT_BACKUP_COMPLETE` | `2` |  |
| `LCS_AUDIT_RESTORE_START` | `3` |  |
| `LCS_AUDIT_RESTORE_COMPLETE` | `4` |  |
| `LCS_AUDIT_VALIDATION_FAILURE` | `5` | source validation-failure audit |
| `LCS_AUDIT_SELF_CONFIG_INVALID` | `6` | self-config-invalid audit |

*lcs_txn state — the transaction-fd state machine state carried in old_state new_state.*

Emitted by lcs:lcs_txn_begin / _first_bind / _bind_mutation _commit /
_abort / _timeout / _source_down.

| Constant | Value | Notes |
|---|---|---|
| `LCS_TXN_ST_ACTIVE_UNBOUND` | `0` | allocated, not yet source-bound |
| `LCS_TXN_ST_ACTIVE_BOUND` | `1` | bound to a source + root guid |
| `LCS_TXN_ST_COMMITTED` | `2` | commit round-trip succeeded |
| `LCS_TXN_ST_ABORTED` | `3` | aborted (close / layer writer abort) |
| `LCS_TXN_ST_TIMED_OUT` | `4` | deadline timer or commit timeout |
| `LCS_TXN_ST_SOURCE_DOWN` | `5` | bound source marked down |

*lcs_key_fd cmd — the key-fd ioctl verb (also stamped on lcs_key_mutation).*

LCS_KCMD_NONE is used by publish/release/read. Never records key/name/SD
bytes. Emitted by lcs:lcs_key_ioctl / _mutation.

| Constant | Value | Notes |
|---|---|---|
| `LCS_KCMD_NONE` | `0` | no ioctl verb (publish/release/read) |
| `LCS_KCMD_SET_VALUE` | `1` |  |
| `LCS_KCMD_DELETE_VALUE` | `2` |  |
| `LCS_KCMD_BLANKET_TOMBSTONE` | `3` |  |
| `LCS_KCMD_DELETE_KEY` | `4` |  |
| `LCS_KCMD_HIDE_KEY` | `5` |  |
| `LCS_KCMD_QUERY_VALUE` | `6` |  |
| `LCS_KCMD_QUERY_VALUES_BATCH` | `7` |  |
| `LCS_KCMD_ENUM_VALUES` | `8` |  |
| `LCS_KCMD_ENUM_SUBKEYS` | `9` |  |
| `LCS_KCMD_QUERY_KEY_INFO` | `10` |  |
| `LCS_KCMD_GET_SECURITY` | `11` |  |
| `LCS_KCMD_SET_SECURITY` | `12` |  |
| `LCS_KCMD_FLUSH` | `13` |  |
| `LCS_KCMD_BACKUP` | `14` |  |
| `LCS_KCMD_RESTORE` | `15` |  |
| `LCS_KCMD_NOTIFY` | `16` |  |

---

# Appendix 5.B LCS ABI Notes

_Peios / Advanced Peios / PKM / LCS_

> What the LCS ABI tables cannot say for themselves — which properties of the interface are documented with their operations rather than here, and the kernel configuration LCS is built by.

§5.A is generated from `pkm/uapi/pkm/lcs.h` and holds only what a
compiler can measure. This appendix holds the rest.

The split is structural rather than editorial. `gen-lcs-abi.py`
overwrites §5.A wholesale on every run, so anything written there is
lost the next time the ABI changes.

## 5.B.1 What is not here

The header carries names, numbers and layouts. Everything else about the
interface is a property of the implementation rather than of the ABI, and
is documented with the operation it belongs to: the required access right
for each ioctl and the two-pass output buffer convention in §5.6, the
error vocabulary in §5.6.4, the RSI payload shapes in the Registry Source
Interface specification, and the backup stream's record payloads in the
Registry Backup Format specification.

`REG_BACKUP_MAGIC` in §5.A is the eight-byte header magic; the record type
codes are the framing, not the payloads.

## 5.B.2 Build configuration

LCS is built by `CONFIG_SECURITY_PKM`, a boolean option, so it is linked
into `vmlinux` rather than loaded. `CONFIG_RUST=y` is required: the
resolution core, the RSI codec, the backup serialiser and the transaction
log are Rust, staged into the kernel tree as `security/pkm/lcs/lcs_core`.
`CONFIG_SECURITY_PKM_KUNIT` compiles in the in-kernel test harness.

The three syscall numbers are added to the syscall table by
`kernel/patches/arch/syscall-table-pkm.patch`, which patches both
`arch/x86/entry/syscalls/syscall_64.tbl` and the copy of it that ships
under `tools/perf/`. They are registered `common`, so they are reachable
from the x32 ABI as well as from x86-64.
