Peios Events Index
Every event Peios emits, in one place — the KMES envelope, the shared subject and process records, and a field-by-field schema for each event from the kernel, the filesystem, the registry, peinit, peipkg and eventd.
Single-page view · as markdown
1.1 What an Event Is
Peios / Using Peios / Events / Introduction
An event is a record that something happened: an access was checked,
a service started, a package was installed, a descriptor was found
corrupt. Events are produced by the component that observed the thing,
and consumed by whatever is watching — usually eventd, sometimes a
tool reading the stream directly.
This book enumerates every event Peios emits, with the fields each one carries. It is a lookup, not an explanation. Where an event's meaning needs the mechanism behind it, the chapter links to the manual that describes that mechanism.
1.1.1 Two transports, not one #
Most events travel through KMES, the kernel message event stream: a per-CPU ring buffer that the kernel writes into and userspace reads from. Every KMES event is a binary header followed by a msgpack payload. Chapters 3 to 7 document KMES events.
Two things in this book are not KMES events, and are included because an operator looking for "what does Peios tell me" would otherwise miss them:
- eventd's synthetic events (§8) are written straight into a shard database and never touch KMES. They carry no header stamps.
- LCS watch records (§5.4) are binary records read from a key file descriptor. They are a notification mechanism, not an audit trail, and their format has nothing in common with a KMES event.
1.1.2 What is not here #
This book does not cover logs or metrics. Both reach eventd by a different path, are stored in different tables, and are not events. The eventd manual covers them.
Nor does it cover the query language for reading events back. That is PSPU §3, with the operator-facing view in the eventd manual.
1.2 The Envelope
Peios / Using Peios / Events / Introduction
Every KMES event is a packed binary header followed immediately by its msgpack payload, delivered as one contiguous byte sequence with no padding anywhere.
The emitter supplies only two things: the event type string and the payload. Everything else in the header is stamped by KMES itself, which is what makes the identity fields trustworthy — an emitter cannot forge them.
1.2.1 Fields KMES stamps #
| Field | Meaning |
|---|---|
timestamp | Wall clock at the moment KMES accepted the event, nanoseconds since the Unix epoch. |
sequence | The emitting CPU's per-boot counter. The first event on each CPU gets 1. |
cpu_id | The CPU whose ring buffer holds the event. |
origin_class | 0 for syscall emission, unconditionally. For kernel emission, the value the calling subsystem passed. |
| identity GUIDs | The effective, true and process token GUIDs of the task that caused the event. |
The identity stamps matter for reading this book: several events carry no caller in their payload at all, because the envelope already names one. StrataFS copy-up records are the clearest case — nothing in the payload names a token, and the caller is recovered from the header.
event_size, header_size and type_len are structural, computed by
KMES during construction.
1.2.2 Layout #
All fields before the event type string sit at fixed offsets. The type
string begins at offset 77, with its u16 length at offset 75, so the
header is exactly 77 + type_len bytes. The payload runs from
header_size to event_size, and the next event begins at
event_size from the start of the current one.
All multi-byte header integers are little-endian. The identity GUIDs are opaque 16-byte values.
The full field-by-field layout is normative in PSPK §2, the KMES event stream specification. This summary is enough to walk a stream; it is not enough to implement one.
1.2.3 Ordering #
timestamp is captured before sequence is assigned, so two events
with the same timestamp on the same CPU are ordered by sequence.
Across CPUs there is no global order. Two events on different CPUs with close timestamps may have been observed in either order.
1.3 Encoding Conventions
Peios / Using Peios / Events / Introduction
Every KMES payload in this book is a msgpack map with UTF-8 string keys. The key set is stable per event type.
1.3.1 Value representations #
The same conceptual types appear across many events and are always encoded the same way.
| Conceptual type | msgpack representation |
|---|---|
| SID | bin holding the binary SID, 8–68 bytes. |
| GUID | bin, exactly 16 bytes. |
| ACE | bin holding the binary ACE, copied from the descriptor. |
| Access mask | uint, 32-bit. |
| Boolean | bool. |
| Privilege name | string, UTF-8, e.g. SeBackupPrivilege. |
| Process ID | uint. |
| Path or name | string, UTF-8. |
| Timestamp | uint, unless an event's schema says otherwise. |
| Object context | bin or nil. An opaque caller-supplied blob; its contents are service-specific. |
Binary SIDs, GUIDs and ACEs are carried as bytes rather than as text because they are compared as bytes. A textual SID would have to be parsed back before it could be matched.
1.3.2 Event type strings use three different styles #
There is no single convention. What an event type looks like depends on which component emits it:
| Style | Emitters | Examples |
|---|---|---|
kebab-case | KACS | access-audit, logon-session-destroyed |
dotted.snake | peinit, peipkg, eventd | job.created, peipkg.repo-add, synthetic.config_change |
SCREAMING_SNAKE | StrataFS, LCS | STRATAFS_COPY_UP, LCS_BACKUP_START |
The dotted family is not internally consistent either: peipkg.repo-add
is dotted-then-kebab while synthetic.config_change is
dotted-then-snake.
This is recorded because a consumer matching event types has to know it, not because it is defended. Match the exact strings in this book rather than deriving one from a pattern.
1.4 Reading an Event
Peios / Using Peios / Events / Introduction
Four rules govern every consumer of this stream.
1.4.1 Ignore unknown keys #
Future versions may add fields to an event without changing the existing ones. A consumer that processes the keys it knows and ignores the rest keeps working across upgrades. A consumer that rejects unrecognised keys breaks on the first addition.
1.4.2 Do not rely on a key being absent #
A field that is optional today may become always-present later. Absence is not a signal.
1.4.3 Delivery is best-effort #
KMES is a ring buffer. The kernel writes; keeping up is the subscriber's problem.
- A subscriber that falls behind loses events. eventd notices and
records a
synthetic.gap(§8.1), which is how a gap becomes visible rather than silent. - There is no replay. An event missed is gone. Nothing can ask for it back.
- Buffers are per-subscriber. One slow reader does not affect another.
- Order is per-subscriber, not global.
For durable audit, read events from eventd's stores rather than from KMES directly. eventd drains its subscription continuously and persists what it reads; from that point the store is the record, not the ring.
1.4.4 Events are not authenticated #
Events are trusted because they came from the kernel through KMES, not because they are signed. Nothing in an event carries a signature.
Cryptographic non-repudiation is a userspace concern applied after events leave the kernel. If a deployment needs it, it is added on the far side of eventd, not here.
1.4.5 Versioning #
Event types are not versioned by a field. The schemas in this book are stable: fields may be added, but an existing field will not be renamed, retyped or removed under the same type string.
A change that would break compatibility changes the type string
instead — access-audit would become access-audit-v2 — so an existing
consumer keeps receiving the shape it understands and simply never sees
the new one. No type in this book has been versioned that way.
2.1 The Subject Record
Peios / Using Peios / Events / Common Records
The subject map identifies the effective token under which an
operation ran. It appears in every KACS event except
logon-session-destroyed.
For an event fired from an impersonating thread, the subject is the impersonation token, not the primary. For a non-impersonating thread the primary token is the effective one.
For continuous-audit (§3.2) the subject is the effective token at
the moment of the operation, not at the moment the handle was opened.
A process whose token changed since the open gets the current subject on
each subsequent operation.
2.1.1 Fields #
| Key | Type | Meaning |
|---|---|---|
user_sid | bin | The token's user SID. |
group_sids | array of bin | The token's group SIDs. |
group_attributes | array of uint | Per-group attribute bitmasks, parallel to group_sids. |
integrity_level | uint | The token's integrity RID — 0, 4096, 8192, 12288 or 16384. |
pip_type | uint | The calling process's PIP type. 0 None, 512 Protected, 1024 Isolated. |
pip_trust | uint | The calling process's PIP trust level. |
auth_id | uint | The LUID of the logon session the token belongs to. |
token_id | uint | The token's own LUID. |
impersonation_level | uint | 0–3. A primary token reports 0. |
projected_uid | uint | The Linux UID projection, for correlating with Linux-side audit data. |
Every field is always present.
auth_id is the join key to logon-session-destroyed (§3.5) and to
/sys/kernel/security/kacs/sessions. token_id correlates events from
one specific token.
2.1.2 The two parallel arrays #
group_sids[i] and group_attributes[i] describe the same group entry,
and the arrays are always the same length.
| Flag | Value | Meaning |
|---|---|---|
SE_GROUP_MANDATORY | 0x01 | Cannot be disabled. |
SE_GROUP_ENABLED_BY_DEFAULT | 0x02 | Enabled at creation. |
SE_GROUP_ENABLED | 0x04 | Currently enabled. |
SE_GROUP_OWNER | 0x08 | May act as owner for new objects. |
SE_GROUP_USE_FOR_DENY_ONLY | 0x10 | Matches deny ACEs only. |
SE_GROUP_INTEGRITY | 0x20 | Identifies an integrity SID. Present for ABI parity; MIC reads the token's integrity_level field, not this flag. |
SE_GROUP_INTEGRITY_ENABLED | 0x40 | Used with SE_GROUP_INTEGRITY. |
SE_GROUP_RESOURCE | 0x20000000 | A domain-local group from a resource domain. Metadata only. |
SE_GROUP_LOGON_ID | 0xC0000000 | The logon SID. Cannot be disabled. |
These are MS-DTYP's names, which PCDS uses. The headers declare the same
flags as KACS_SID_GROUP_*; the Peios Kernel TRM §3.A maps the two.
Reconstructing group membership from an event means applying the same
rule the access check applies: SE_GROUP_ENABLED set and
SE_GROUP_USE_FOR_DENY_ONLY clear, for allow-side matching.
2.1.3 What the subject deliberately omits #
Privileges, claims, the restricted-SID list, confinement state, and the default DACL are all absent. Each is unbounded, and an event that embedded them could grow without limit.
Code needing full token state queries the token directly with
KACS_IOC_QUERY, while it still exists. The subject record is for
correlation, not for reconstruction.
2.2 The Process Record
Peios / Using Peios / Events / Common Records
The process map identifies the process the event came from. It appears
in every KACS event except logon-session-destroyed, which has no
causing process.
| Key | Type | Meaning |
|---|---|---|
pid | uint | The process ID. |
name | string | The kernel's name for the process, typically the executable's basename. |
executable_path | string | The path resolved at exec, with symlinks already followed. |
Every field is always present. For continuous-audit this is the
operation-time process, not the one that opened the handle.
2.2.1 Correlating on it #
pid is reliable only in the short term. Process IDs are reused, so a
pid in a week-old record may name something unrelated. For durable
records, correlate on name and executable_path.
name is the kernel's internal name. It is not argv[0], and a process
that rewrote its argv is unaffected here.
2.2.2 No thread ID #
The record identifies a process, not a thread. Events that fire on one
specific thread still report only the process. Nothing in the current
event set carries a tid.
2.3 The Caller Summary
Peios / Using Peios / Events / Common Records
LCS uses its own identity submap, caller, rather than the subject
record. Six of its seven audit events carry it.
It exists separately because LCS's events are emitted from a different subsystem with a different bound on what it will serialise. The two records overlap but are not interchangeable, and a consumer handling both needs to read each on its own terms.
| Key | Meaning |
|---|---|
effective_token_guid | The token the operation ran under. |
true_token_guid | The underlying token, where impersonation is in play. |
process_guid | The calling process. |
user_sid | The effective token's user SID. |
authentication_id | The logon session LUID. |
token_id | The token's own LUID. |
token_type | Primary or impersonation. |
impersonation_level | 0 for a primary token. |
integrity_level | The token's integrity RID. |
Nine fields, and no more. Group lists, privilege arrays, claims and default DACLs are unbounded and are never included — the same reasoning that shapes the subject record (§2.1), applied independently.
2.3.1 Against the subject record #
subject | caller | |
|---|---|---|
| Emitted by | KACS | LCS |
| Identity by | SIDs | GUIDs, plus user_sid |
| Groups | group_sids with attributes | absent |
| PIP state | pip_type, pip_trust | absent |
| Linux projection | projected_uid | absent |
| Session join key | auth_id | authentication_id |
Note the session join key is spelled differently in each. Correlating a
KACS event with an LCS event on the same logon session means matching
subject.auth_id against caller.authentication_id.
3.1 access-audit
Peios / Using Peios / Events / Kernel Access Events
The most common event in the system. Fires at AccessCheck completion,
from the SACL audit walk, and from a token's audit_policy forcing an
audit that no ACE asked for.
Event type string: access-audit.
| Key | Type | Meaning |
|---|---|---|
subject | map | Subject record (§2.1). |
object_context | bin or nil | Caller-supplied opaque identifier for the object. nil if AccessCheck was not given one. |
requested_access | uint | The mask the caller requested, after generic mapping. |
granted_access | uint | The mask actually granted. |
success | bool | True when every requested bit is in granted_access. |
trigger | map | Why this event fired. Below. |
process | map | Process record (§2.2). |
Every field is always present.
3.1.1 The trigger record #
| Key | Type | Meaning |
|---|---|---|
kind | string | sacl or policy. |
ace | bin or nil | For kind = sacl, the matched ACE's bytes. For kind = policy, nil. |
kind = sacl means an audit ACE in the object's SACL — or in a central
access policy's SACL — matched this access, and ace carries the exact
ACE so a consumer can identify which rule fired.
kind = policy means nothing in any SACL asked for this. The audit
fired because the calling token's audit_policy carries
OBJECT_ACCESS_SUCCESS or OBJECT_ACCESS_FAILURE, which forces an
audit on every access that token makes.
The distinction matters when reading volume. A flood of kind = policy
events is a property of the token, and is fixed by changing the token's
policy. A flood of kind = sacl events is a property of the object, and
is fixed by changing its SACL.
3.1.2 One access can produce several events #
The event is per matching audit ACE, not per access. An access that
matches three audit ACEs produces three events, each with a different
ace in its trigger and otherwise identical.
3.1.3 Example #
A successful read where a SACL audit ACE matched:
{
"event_type": "access-audit",
"event_time": <timestamp>,
"subject": { ... },
"object_context": <bin>,
"requested_access": 0x00120089,
"granted_access": 0x00120089,
"success": true,
"trigger": { "kind": "sacl", "ace": <bin> },
"process": { ... }
}
0x00120089 is GENERIC_READ after mapping to file-specific bits.
Generic bits never survive into an event; the mask is always specific.
3.2 continuous-audit
Peios / Using Peios / Events / Kernel Access Events
Fires per operation on an already-open handle, when the operation's required access overlaps a continuous audit mask cached on that handle.
Where access-audit records the decision to open something,
continuous-audit records what was then done with it. The mask is
configured by SYSTEM_ALARM* ACEs at the access check that opened the
handle, and the event is fired afterwards by whatever kernel subsystem
enforces the operation — FACS for file handles.
Event type string: continuous-audit.
| Key | Type | Meaning |
|---|---|---|
subject | map | Subject record (§2.1), reflecting the operation-time effective token. |
object_context | bin or nil | Object identifier. May differ from the open-time context if the enforcement point keeps its own. |
operation | string | The operation name. FACS uses a file. prefix — file.read, file.write, file.fallocate. Other enforcement points use their own. |
requested_access | uint | The mask this specific operation needs. |
matched_access | uint | The subset of requested_access that overlapped the handle's continuous audit mask. |
granted_access | uint | The mask cached on the handle at open time. |
success | bool | Whether the operation itself succeeded. |
process | map | Process record (§2.2), operation-time. |
Every field is always present.
3.2.1 The three masks #
They are easy to confuse, and reading them together is the point of the event.
requested_accessis what the operation needs. A read needsFILE_READ_DATA; a write needsFILE_WRITE_DATA; anmmapwithPROT_EXECneedsFILE_EXECUTE.matched_accessisrequested_accessintersected with the handle's continuous audit mask. This is why the event exists — the bits that triggered it.granted_accessis what the handle was opened with. An operation can only succeed ifrequested_accessis a subset of it.
Read as a sentence: the operation needed these bits, of which these triggered audit, against a handle opened with these, and it succeeded or did not.
3.2.2 Why the subject is re-read #
A handle outlives the token that opened it. A process that changes its effective token — starting or stopping impersonation — keeps its handles, and every subsequent operation is audited under the token current at that moment.
An investigation that assumes the open-time identity for later operations will attribute them to the wrong principal.
3.2.3 Example #
A read on a file carrying an alarm ACE on FILE_READ_DATA:
{
"event_type": "continuous-audit",
"event_time": <timestamp>,
"subject": { ... },
"object_context": <bin>,
"operation": "file.read",
"requested_access": 0x00000001,
"matched_access": 0x00000001,
"granted_access": 0x00120089,
"success": true,
"process": { ... }
}
3.3 privilege-use
Peios / Using Peios / Events / Kernel Access Events
Fires at AccessCheck for a privilege that contributed bits to the
granted mask, when the token's audit_policy asks for it.
Event type string: privilege-use.
| Key | Type | Meaning |
|---|---|---|
subject | map | Subject record (§2.1). |
object_context | bin or nil | Object identifier from the access check. |
privilege | string | The canonical privilege name. |
requested_access | uint | The bits the caller requested that this privilege might address. |
granted_access | uint | The bits the privilege contributed, before later narrowing. |
surviving_access | uint | The subset of granted_access that reached the final granted mask. |
success | bool | True when surviving_access is non-empty. |
process | map | Process record (§2.2). |
Every field is always present.
3.3.1 Only five privileges can appear #
The privilege field carries a canonical name, and only five are
representable:
SeSecurityPrivilegeSeTakeOwnershipPrivilegeSeBackupPrivilegeSeRestorePrivilegeSeRelabelPrivilege
Any other bit fails the encoder closed rather than emitting an unnamed privilege. This is consistent with those being the only five that can influence an access check at all, and therefore the only five that can produce this event.
A consumer will never see a sixth name here. A privilege used for
something other than an access decision produces no privilege-use
event.
3.3.2 Success means it worked, not that it fired #
This is the field most often misread.
success = true — the privilege contributed bits and they survived to
the final grant. The privilege did useful work. Governed by the
PRIVILEGE_USE_SUCCESS audit policy.
success = false — the privilege contributed bits and a later layer
stripped them. The caller was in a confinement that does not permit it,
or a CAAP rule narrowed it out, or the caller was non-dominant under
PIP. Governed by PRIVILEGE_USE_FAILURE.
A success = false event is not a failed attempt to use a privilege.
It is a privilege that fired and was then overridden — which is usually
the more interesting record of the two, because it shows a boundary
doing its job.
A token can carry either policy bit, both, or neither, and each controls its own flavour independently.
3.3.3 Example #
A backup tool whose SeBackupPrivilege was stripped by confinement:
{
"event_type": "privilege-use",
"event_time": <timestamp>,
"subject": { ... },
"object_context": <bin>,
"privilege": "SeBackupPrivilege",
"requested_access": 0x00000001,
"granted_access": 0x00000001,
"surviving_access": 0x00000000,
"success": false,
"process": { ... }
}
3.4 caap-policy-diagnostic
Peios / Using Peios / Events / Kernel Access Events
Fires during the central access policy step of AccessCheck, for two
unrelated conditions distinguished by the kind field: a CAAP SACL that
failed to evaluate, and a staged policy that would have decided
differently from the effective one.
Event type string: caap-policy-diagnostic.
| Key | Type | Meaning |
|---|---|---|
subject | map | Subject record (§2.1). |
object_context | bin or nil | Caller-supplied object identifier. |
kind | string | sacl-error or staging-mismatch. |
phase | string | Which phase of CAAP evaluation produced the diagnostic. |
policy_sid | bin or nil | The policy involved. nil for staging-mismatch. |
rule_index | uint or nil | The rule involved. nil for staging-mismatch. |
reason | string or nil | Diagnostic text, for sacl-error. |
requested_access | uint | The mask the caller requested. |
effective_granted_access | uint | Total granted under the effective policy. |
staged_granted_access | uint | Total the staged policy would have granted. |
object_results_differ | bool | Whether staged and effective differed for this object. |
process | map | Process record (§2.2). |
Every key is always present; several are nil depending on kind.
3.4.1 kind = sacl-error #
A central access rule's SACL could not be evaluated. policy_sid,
rule_index and reason identify what failed and where.
This is a defect in the policy, not in the access. The access is still decided; the event says a rule that should have participated could not.
3.4.2 kind = staging-mismatch #
A staged policy — one being trialled before it takes effect — would have produced a different result from the policy actually in force. This is the mechanism's whole purpose: run the new policy in parallel and report where it would have changed something, before it can break anything.
policy_sid and rule_index are nil here. The mismatch is a property
of the whole evaluation, not attributable to one rule.
3.4.3 The limits of the mismatch report #
Worth knowing before building anything on it.
It carries the two total granted masks and a boolean. It does not identify which rule differed, and it does not report which audit events would have changed.
For a mismatch that is purely in SACL behaviour, the two masks can be
equal while object_results_differ is true. A consumer comparing
only the masks will conclude nothing changed.
3.5 logon-session-destroyed
Peios / Using Peios / Events / Kernel Access Events
Fires when a logon session loses its last token reference and the kernel destroys it.
Event type string: logon-session-destroyed.
| Key | Type | Meaning |
|---|---|---|
session_id | uint | The destroyed session's LUID. |
user_sid | bin | The session's user SID. |
logon_type | uint | Interactive, Network, and so on. |
auth_package | string | The authenticating package — Kerberos, NTLM, local. |
created_at | uint | When the session was created. |
Every field is always present.
3.5.1 No subject, no process #
This is the only KACS event with neither. The session was the subject, and it has just ended. No process caused it — the last reference simply went away, which may have been any process exiting, or none in particular.
3.5.2 Correlating #
session_id is the same LUID that every token in that session reported
as subject.auth_id (§2.1), and that LCS events report as
caller.authentication_id (§2.3). It is the join key for everything
that session ever did.
With created_at, the event bounds a session's whole lifetime, which is
what makes it useful for reconstructing a login after the fact.
The event fires exactly once per session. Consumers — authd especially — use it to release session-scoped state: Kerberos tickets, cached directory data, per-session credentials.
3.5.3 There is no matching creation event #
Nothing is emitted when a session is created, so the pair is asymmetric. See §3.6 for what else is absent and why.
3.6 corrupt-sd
Peios / Using Peios / Events / Kernel Access Events
Fires when FACS encounters a structurally invalid security descriptor on a file.
Event type string: corrupt-sd.
| Key | Type | Meaning |
|---|---|---|
subject | map | Subject record (§2.1) for the access that triggered detection. |
object_context | bin or nil | Identifier of the object whose descriptor was corrupt. |
reason | string | What was wrong — sd_too_large, acl_malformed, sid_invalid. |
process | map | Process record (§2.2). |
Every field is always present.
The subject is whoever happened to touch the file, not whoever caused the corruption. Nothing records that.
3.6.1 Rate-limited by design #
One event per inode per cache population. A corrupt descriptor read a thousand times during one mount's life produces one event, not a thousand.
This is deliberate: a filesystem with many corrupt descriptors would otherwise drown the audit stream at exactly the moment the stream is most needed. The consequence is that event count says nothing about access count — one event does not mean one access.
3.6.2 Informational only #
The kernel denies the access regardless. A corrupt descriptor fails closed, and this event is not part of that decision — it is what tells an administrator the corruption exists at all.
Without it, a file with a broken descriptor is simply inaccessible, with nothing anywhere explaining why.
3.6.3 Events that do not exist #
Three absences in the KACS set are worth stating, because each is a reasonable thing to look for:
- No
logon-session-created. Sessions are announced only when they end (§3.5). Track creation through authd's own records or by polling/sys/kernel/security/kacs/sessions. - No
token-created. A token's existence is observable through process inspection, not through an event. - No periodic or heartbeat events. Audit here is entirely event-driven. A silent stream means nothing happened, not that anything is broken.
These are intentional. The kernel's audit surface covers access decisions and session endings; other lifecycle tracking belongs to the layers above it.
4.1 STRATAFS_COPY_UP
Peios / Using Peios / Events / Filesystem Events
Fires on every StrataFS copy-up, successful or not.
Event type string: STRATAFS_COPY_UP.
| Key | Meaning |
|---|---|
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. |
Six keys, and no seventh.
4.1.1 No caller in the payload #
Nothing here names a token, and that is not an omission.
Copy-up preserves the source object's descriptor, so the resulting file records nothing about who caused it to exist. The caller is recovered from the envelope instead: KMES stamps the effective, true and process token GUIDs onto the header at ring-write time, and because copy-up runs in the caller's own context, those are the caller's (§1.2).
A consumer reading only payloads will conclude these events are anonymous. They are not — the identity is one level out.
4.1.2 ENOTDIR arrives here, not in the refusal event #
A parent materialisation that fails with ENOTDIR is reported through
this event, carried in result_errno, rather than through
STRATAFS_MUTATION_REFUSED (§4.2).
That is worth knowing when searching for a refusal and finding nothing: the record exists, under a different type, with all the required fields present.
4.2 STRATAFS_MUTATION_REFUSED
Peios / Using Peios / Events / Filesystem Events
Fires when a mutation is refused because of how the mount is arranged, rather than because of an access check.
Event type string: STRATAFS_MUTATION_REFUSED.
| Key | Meaning |
|---|---|
path | The relative path within the mount. |
operation | The operation name. |
provider_index | The provider stratum's index. |
provider_stratum | That stratum's path. |
errno | The refusal. |
| deferred flag | Whether the refusal was deferred. |
4.2.1 What counts as an arrangement refusal #
One explicit list, matching the specification's enumeration exactly:
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, removexattr, creation, tmpfile, unlink, rmdir, link,
supersede and rename.
EACCES is deliberately absent. A refusal produced by an access
check is audited by the mechanism that performed it, and StrataFS does
not duplicate those records. Looking here for a permission denial finds
nothing; look for access-audit (§3.1) instead.
4.2.2 What these records are for #
They 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 configuration, and it is otherwise visible only as an error returned to a caller that may well discard it.
Rollbacks are audited under this same type 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.
4.2.3 Two irregularities #
A refusal raised before a provider is known — creation, tmpfile, the
heads of link and rename — passes a provider index of -1, so
provider_stratum is emitted as an empty string. The specification asks
for the provider stratum in every refusal record, so this is a gap
rather than a design.
A refused deferred deletion is audited on any non-zero result, not
only on the arrangement errors, so one refused by an access check does
produce a StrataFS record. This is a deliberate exception to the
EACCES exclusion above: the requirement to audit a deferred deletion
is unconditional, because by then nobody is left to receive the error.
4.2.4 What is not audited #
Resolution, revalidation and enumeration emit nothing. They occur on every path operation, 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 against provider objects are audited by KACS under its own rules.
5.1 Registry Events
Peios / Using Peios / Events / Registry Events
LCS emits seven audit events through KMES. Six of them carry the caller summary (§2.3) rather than the subject record.
| 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. |
Every payload is a msgpack map with string keys. GUIDs are 16-byte binary values; SIDs are binary KACS encodings.
5.1.1 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 at all.
The start/complete pairing is deliberate too. LCS_BACKUP_START is
emitted before any data is read and LCS_RESTORE_START before any
state is modified, so an operation that dies partway still leaves
evidence that it began.
5.1.2 Separately: watch records #
LCS also produces watch records, read from a key file descriptor. These are not KMES events, not msgpack, and not audit — see §5.4.
5.2 LCS_KEY_OPEN_AUDIT
Peios / Using Peios / Events / Registry Events
Fires when a key open matched a SACL audit ACE.
| Key | Meaning |
|---|---|
caller | Caller summary (§2.3). |
| key GUID | The key that was opened. |
requested_access | The mask after registry generic mapping, with MAXIMUM_ALLOWED re-added if the caller asked for it. |
granted_access | The mask granted. Forced to zero on a denial. |
| decision | allowed or denied. |
sacl_match_flags | Bit 0 for a success-audit match, bit 1 for a failure-audit match. No other bits. |
granted_access being zero on a denial is enforced, not merely
intended: a denied event carrying a non-zero granted mask is rejected as
a malformed payload.
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, itself gated by
SeSecurityPrivilege.
5.2.1 One matching SACL that produces nothing #
A request of MAXIMUM_ALLOWED alone maps to a desired mask of zero.
AccessCheck's SACL walk tests each audit ACE's mask against the mapped
desired access, and no ACE matches zero.
So an open with a matching audit ACE emits no event, and LCS emits nothing. This is a real hole in coverage for anyone auditing key opens: the one request shape that asks for everything is the one that records nothing.
5.2.2 When emission fails #
The policy is specific to this event, and differs from the bulk ones.
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. The audit is a precondition of the
access.
If the payload is valid but KMES cannot retain it — unavailable,
ring drops, capacity pressure, no consumer — the access decision and the
fd publication are unaffected. Loss accounting is KMES's problem, and
shows up as a synthetic.gap (§8.1) rather than as a failed open.
5.3 Validation and Configuration Failures
Peios / Using Peios / Events / Registry Events
5.3.1 LCS_SOURCE_VALIDATION_FAILURE #
Fires when LCS rejects malformed source data.
Carries the source slot identifier, 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 classes:
| Group | Classes |
|---|---|
| Name fields | malformed_layer_name, malformed_key_name, malformed_value_name |
| Structural | malformed_response_payload, malformed_key_metadata, malformed_value_payload, malformed_delete_layer_orphan_list |
| Descriptors | malformed_security_descriptor, malformed_layer_metadata_security_descriptor |
| Sequencing | future_sequence_number, duplicate_winning_sequence_tie |
| Protocol | unknown_rsi_status_code |
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.3.2 LCS_SELF_CONFIG_INVALID #
Fires when LCS rejects an invalid self-configuration value.
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.
5.3.2.1 Expect nineteen of these on a first boot #
Because missing counts as invalid, a first boot before seed restore
emits one event per parameter on each refresh: nineteen events
against an empty Registry\ key.
That is correct and expected, and it is a noticeable share of the boot audit stream. A consumer alerting on validation failures needs to know it, or every fresh machine looks like it is failing.
5.4 Watch Records
Peios / Using Peios / Events / Registry Events
Watch records are not KMES events. They are binary records read from
a key file descriptor with read(), and they exist to tell a watcher
that something under a key changed.
They are documented here because an operator asking what the registry reports would otherwise miss them, but nothing else in this book applies to them: no msgpack, no envelope, no identity stamps, no audit meaning.
5.4.1 Reading #
A single read() returns as many complete records as fit in the
caller's buffer. A record is never split across two calls.
If the buffer cannot hold even the first queued record, read() fails
with EINVAL — the buffer is too small to make progress, and the caller
retries with a larger one. On an armed fd with an empty queue, read()
blocks, or returns EAGAIN under O_NONBLOCK.
Only records copied out in full are dequeued.
5.4.2 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 record,
and it is the only safe way to do so.
5.4.3 Subtree watches carry a path #
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: a u16 length, 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 by a separator because registry names can contain any Unicode character and value names can contain backslashes. A concatenated path string would be ambiguous.
OVERFLOW records are emitted in the bare eight-byte form, with no path
even on a subtree watch — there is no single key to name when the queue
itself overflowed.
The header offsets are ABI, named in uapi/pkm/lcs.h and listed in the
Peios Kernel TRM §5.A.
6.1 Job Events
Peios / Using Peios / Events / Service Events
peinit emits a structured event at every job and operation lifecycle transition. All of them go into the KMES ring buffer, encoded as msgpack per the envelope in §1.2.
There is no event socket. Structured events are not sent to eventd over any connection. eventd consumes them from the ring buffer, which is why they survive eventd being down, restarted, or not yet existing. The only thing peinit sends eventd over a socket is service output, which is a different path with different guarantees.
6.1.1 The three job events #
| Event | Fires when | Carries |
|---|---|---|
job.created | The job object exists. | Job identifier, service name, type, image path, identity, operation identifier. |
job.started | exec succeeded. | Job identifier, PID, cgroup path. |
job.ended | The process exited or was killed. | Job identifier, final state, exit code or signal, duration, failure cause. |
The event type is the dotted string; the fields form the msgpack
payload. The payloads are supersets of the summaries above — job.ended
in particular carries the whole record.
6.1.2 Ordering #
When one runtime step produces several lifecycle events, peinit emits them in causal order before committing the retained state for that step. A consumer sees the events in an order consistent with what happened, not in whatever order the writes completed.
6.2 Operation Events
Peios / Using Peios / Events / Service Events
Every operation event carries the same five fields: operation_id,
type, service, source, caller — null for a
lifecycle-generated operation — and state after the transition.
Seven types, each adding its own fields:
| Event | Adds |
|---|---|
operation.requested | — |
operation.started | — |
operation.completed | duration_ns, result |
operation.failed | duration_ns, failure_reason |
operation.cancelled | reason |
operation.merged | merged_into |
operation.aborted | duration_ns, reason |
6.2.1 duration_ns measures from creation #
Not from the start of execution. The reasoning is the same as for the operation timeout: what a caller waited is what matters, and queue time is part of it.
An operation that sat in a queue for a minute and then ran for a second reports 61 seconds, not 1.
6.2.2 One field, three names #
The same value appears under three spellings across two surfaces:
failure_reasononoperation.failedreasononoperation.cancelledandoperation.abortederrorin the control interface's operation view (PSPU §4)
A consumer correlating an event stream against the control interface has to map all three onto each other.
7.1 The Event Set
Peios / Using Peios / Events / Package Events
peipkg emits eleven event types into the kernel event subsystem. The caller's identity is stamped into the envelope by the kernel (§1.2) and is not part of the payload.
| Type | Emitted for | Emitted today |
|---|---|---|
peipkg.install | A successful install | yes |
peipkg.upgrade | A successful upgrade, downgrade, or undo | yes |
peipkg.uninstall | A successful uninstall | yes |
peipkg.refresh | A repository refresh, successful or partially failed | yes |
peipkg.transaction-failed | A rejected or rolled-back transaction | yes |
peipkg.recovery | A recovery resolved through peipkg recover | yes |
peipkg.authorisation | An operator authorisation record | yes |
peipkg.repo-add | A repository add | yes |
peipkg.repo-remove | A repository remove | yes |
peipkg.claim | A claim grant or revoke | yes |
peipkg.config-change | A trust-policy or transport-flag change | no |
peipkg.config-change is specified and not emitted. A trust-policy
or transport-flag change today produces no event at all.
7.1.1 Payload fields #
| Field | Content |
|---|---|
txn_id | The transaction identifier. |
outcome | success, rejection, or rollback. |
repo | The repository, for repository operations. |
detail | The rejection reason, the operation count, or the authorised action. |
timestamp | RFC 3339, UTC. |
packages | Name, version and architecture per package. |
Note timestamp here is an RFC 3339 string, not the uint used
elsewhere in this book (§1.3). peipkg carries its own.
7.1.2 Emission depends on a privilege #
An audit privilege on the caller's token. Without it, emission fails, peipkg warns, and the operation proceeds unaudited. The absence of an event does not mean the operation did not happen.
On a kernel with no emit call, emission is a silent successful no-op.
7.2 What Is Not Recorded
Peios / Using Peios / Events / Package Events
The gaps in peipkg's audit trail are specific, and each one is a place where something happened and nothing was written.
- An install or upgrade event carries no source repository, although one is known at the time.
- A committed cross-root operation's success event carries no transaction identifier, so it cannot be correlated with the rest of its transaction.
- Automatic recovery at the head of an ordinary operation emits
nothing. Only recovery through
peipkg recoverproducespeipkg.recovery. peipkg recover's failure paths emit nothing. A recovery that fails leaves no record that it was attempted.- Declining at a prompt emits nothing.
- Enabling insecure transport emits no authorisation record, and so
does installing unsigned content under an
optionalpolicy — the two decisions most worth recording are the two that are not. peipkg-composeemits nothing at all. Image composition is entirely unaudited.
7.2.1 Reading the trail with these in mind #
Two consequences follow for anyone building on this stream.
Absence is not evidence. An operation with no event may have been performed by a caller without the audit privilege, may have taken one of the paths above, or may not have happened. The three are indistinguishable from the stream.
Transaction correlation is incomplete. txn_id ties an operation's
events together, but the cross-root success case omits it, so a
reconstruction keyed on txn_id will silently drop those.
8.1 Synthetic Events
Peios / Using Peios / Events / Event Daemon Events
Synthetic events are records eventd generates about itself. They are written straight into a shard database and never touch KMES.
They carry no KMES header: no identity stamps, no sequence number, no
origin class. What they have is a wall-clock timestamp taken when eventd
generated the record, and a type string prefixed synthetic., which is
what distinguishes them in the events table — there is no separate
record-type column.
| Condition | Type |
|---|---|
| Lost events detected on a CPU | synthetic.gap |
| eventd started and attached to KMES | synthetic.startup |
| Graceful shutdown beginning | synthetic.shutdown |
| A write to any store failed | synthetic.storage_error |
| A configuration value changed at runtime | synthetic.config_change |
Five, and no more. Payload schemas are in the eventd manual §3.2.
8.1.1 There is no event for bad input #
Deliberately. Log and metric datagrams arrive unauthenticated from arbitrary local processes, and emitting a durable record per bad datagram would hand every process an amplification primitive (PSPU §3.4).
These five are conditions eventd observed about itself, not reactions to what it was sent.
8.1.2 Which shard they land in #
CPU-specific — synthetic.gap — goes to the shard assigned to the
CPU that generated it, handed to that writer thread alongside that CPU's
ordinary events. A gap record travels with the events it describes.
Daemon-wide — startup, shutdown, config changes, storage errors — go to shard 0 when shard 0 is writable, otherwise to the lowest-numbered writable active shard. If no shard is writable, the event is skipped and the failure is logged to standard error.
A storage error is why the fallback exists. It describes a failure on one shard but is itself a daemon-wide notification, so it is not written to the failing shard unless that shard has since been replaced and is writable again. Writing the record of a shard's failure into that shard would lose it exactly when it matters.
8.1.3 The timestamp is when eventd noticed #
Not when the condition occurred.
A synthetic.gap is stamped at detection, which may be long after
the events it describes were overwritten — and after a restart it may be
the first thing written in a new boot about events lost in the previous
one.
An investigation that treats a gap record's timestamp as the time of the loss will look in the wrong window.
8.1.4 Storage and ordering #
Synthetic events live in the same shard databases as KMES events and
take part in the same batching, retention and queries. Access control
treats their types like any other, so
Machine\System\eventd\Security\Events\synthetic governs them.
They are ordered by their eventd-assigned timestamp and take no part in per-CPU sequence numbering.
Appendix A All Event Types
Peios / Using Peios / Events
Every event type in this book, in one table. Thirty-four types across six emitters, plus the registry's watch records, which are a separate mechanism.
A.1 KMES events #
| Type | Emitter | Where |
|---|---|---|
access-audit | KACS | §3.1 |
continuous-audit | KACS | §3.2 |
privilege-use | KACS | §3.3 |
caap-policy-diagnostic | KACS | §3.4 |
logon-session-destroyed | KACS | §3.5 |
corrupt-sd | KACS | §3.6 |
STRATAFS_COPY_UP | StrataFS | §4.1 |
STRATAFS_MUTATION_REFUSED | StrataFS | §4.2 |
LCS_KEY_OPEN_AUDIT | LCS | §5.2 |
LCS_BACKUP_START | LCS | §5.1 |
LCS_BACKUP_COMPLETE | LCS | §5.1 |
LCS_RESTORE_START | LCS | §5.1 |
LCS_RESTORE_COMPLETE | LCS | §5.1 |
LCS_SOURCE_VALIDATION_FAILURE | LCS | §5.3 |
LCS_SELF_CONFIG_INVALID | LCS | §5.3 |
job.created | peinit | §6.1 |
job.started | peinit | §6.1 |
job.ended | peinit | §6.1 |
operation.requested | peinit | §6.2 |
operation.started | peinit | §6.2 |
operation.completed | peinit | §6.2 |
operation.failed | peinit | §6.2 |
operation.cancelled | peinit | §6.2 |
operation.merged | peinit | §6.2 |
operation.aborted | peinit | §6.2 |
peipkg.install | peipkg | §7.1 |
peipkg.upgrade | peipkg | §7.1 |
peipkg.uninstall | peipkg | §7.1 |
peipkg.refresh | peipkg | §7.1 |
peipkg.transaction-failed | peipkg | §7.1 |
peipkg.recovery | peipkg | §7.1 |
peipkg.authorisation | peipkg | §7.1 |
peipkg.repo-add | peipkg | §7.1 |
peipkg.repo-remove | peipkg | §7.1 |
peipkg.claim | peipkg | §7.1 |
peipkg.config-change | peipkg | §7.1 — specified, not emitted |
A.2 Not KMES #
| Type | Emitter | Transport | Where |
|---|---|---|---|
synthetic.gap | eventd | Written direct to a shard | §8.1 |
synthetic.startup | eventd | Written direct to a shard | §8.1 |
synthetic.shutdown | eventd | Written direct to a shard | §8.1 |
synthetic.storage_error | eventd | Written direct to a shard | §8.1 |
synthetic.config_change | eventd | Written direct to a shard | §8.1 |
| Watch records | LCS | read() on a key fd | §5.4 |
A.3 Which carry an identity, and how #
| Events | Identity from |
|---|---|
KACS, all but logon-session-destroyed | subject record in the payload (§2.1) |
logon-session-destroyed | user_sid and session_id directly; the session was the subject |
| LCS, six of seven | caller summary in the payload (§2.3) |
| StrataFS, peinit, peipkg | The envelope only. No identity in the payload (§1.2) |
| eventd synthetic | None. eventd is describing itself |
A.4 Known holes #
Collected from the chapters, because a reader planning coverage needs them in one place:
peipkg.config-changeis never emitted (§7.1).- peipkg omits the source repository on install and upgrade, the
transaction id on committed cross-root success, and emits nothing for
automatic recovery, recovery failures, declined prompts, insecure
transport, unsigned installs under an
optionalpolicy, orpeipkg-compose(§7.2). - A registry key open requesting
MAXIMUM_ALLOWEDalone emits noLCS_KEY_OPEN_AUDIT, because the mapped desired mask is zero and no ACE matches zero (§5.2). - StrataFS refusals raised before a provider is known emit an empty
provider_stratum(§4.2). caap-policy-diagnosticwithkind = staging-mismatchdoes not identify which rule differed, and its two masks can be equal whileobject_results_differis true (§3.4).- There is no
logon-session-created, notoken-created, and no heartbeat (§3.6). - peipkg emits nothing when the caller's token lacks the audit privilege (§7.1).