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

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 #

FieldMeaning
timestampWall clock at the moment KMES accepted the event, nanoseconds since the Unix epoch.
sequenceThe emitting CPU's per-boot counter. The first event on each CPU gets 1.
cpu_idThe CPU whose ring buffer holds the event.
origin_class0 for syscall emission, unconditionally. For kernel emission, the value the calling subsystem passed.
identity GUIDsThe 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 typemsgpack representation
SIDbin holding the binary SID, 8–68 bytes.
GUIDbin, exactly 16 bytes.
ACEbin holding the binary ACE, copied from the descriptor.
Access maskuint, 32-bit.
Booleanbool.
Privilege namestring, UTF-8, e.g. SeBackupPrivilege.
Process IDuint.
Path or namestring, UTF-8.
Timestampuint, unless an event's schema says otherwise.
Object contextbin 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:

StyleEmittersExamples
kebab-caseKACSaccess-audit, logon-session-destroyed
dotted.snakepeinit, peipkg, eventdjob.created, peipkg.repo-add, synthetic.config_change
SCREAMING_SNAKEStrataFS, LCSSTRATAFS_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 #

KeyTypeMeaning
user_sidbinThe token's user SID.
group_sidsarray of binThe token's group SIDs.
group_attributesarray of uintPer-group attribute bitmasks, parallel to group_sids.
integrity_leveluintThe token's integrity RID — 0, 4096, 8192, 12288 or 16384.
pip_typeuintThe calling process's PIP type. 0 None, 512 Protected, 1024 Isolated.
pip_trustuintThe calling process's PIP trust level.
auth_iduintThe LUID of the logon session the token belongs to.
token_iduintThe token's own LUID.
impersonation_leveluint0–3. A primary token reports 0.
projected_uiduintThe 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.

FlagValueMeaning
SE_GROUP_MANDATORY0x01Cannot be disabled.
SE_GROUP_ENABLED_BY_DEFAULT0x02Enabled at creation.
SE_GROUP_ENABLED0x04Currently enabled.
SE_GROUP_OWNER0x08May act as owner for new objects.
SE_GROUP_USE_FOR_DENY_ONLY0x10Matches deny ACEs only.
SE_GROUP_INTEGRITY0x20Identifies an integrity SID. Present for ABI parity; MIC reads the token's integrity_level field, not this flag.
SE_GROUP_INTEGRITY_ENABLED0x40Used with SE_GROUP_INTEGRITY.
SE_GROUP_RESOURCE0x20000000A domain-local group from a resource domain. Metadata only.
SE_GROUP_LOGON_ID0xC0000000The 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.

KeyTypeMeaning
piduintThe process ID.
namestringThe kernel's name for the process, typically the executable's basename.
executable_pathstringThe 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.

KeyMeaning
effective_token_guidThe token the operation ran under.
true_token_guidThe underlying token, where impersonation is in play.
process_guidThe calling process.
user_sidThe effective token's user SID.
authentication_idThe logon session LUID.
token_idThe token's own LUID.
token_typePrimary or impersonation.
impersonation_level0 for a primary token.
integrity_levelThe 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 #

subjectcaller
Emitted byKACSLCS
Identity bySIDsGUIDs, plus user_sid
Groupsgroup_sids with attributesabsent
PIP statepip_type, pip_trustabsent
Linux projectionprojected_uidabsent
Session join keyauth_idauthentication_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.

KeyTypeMeaning
subjectmapSubject record (§2.1).
object_contextbin or nilCaller-supplied opaque identifier for the object. nil if AccessCheck was not given one.
requested_accessuintThe mask the caller requested, after generic mapping.
granted_accessuintThe mask actually granted.
successboolTrue when every requested bit is in granted_access.
triggermapWhy this event fired. Below.
processmapProcess record (§2.2).

Every field is always present.

3.1.1 The trigger record #

KeyTypeMeaning
kindstringsacl or policy.
acebin or nilFor 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.

KeyTypeMeaning
subjectmapSubject record (§2.1), reflecting the operation-time effective token.
object_contextbin or nilObject identifier. May differ from the open-time context if the enforcement point keeps its own.
operationstringThe operation name. FACS uses a file. prefix — file.read, file.write, file.fallocate. Other enforcement points use their own.
requested_accessuintThe mask this specific operation needs.
matched_accessuintThe subset of requested_access that overlapped the handle's continuous audit mask.
granted_accessuintThe mask cached on the handle at open time.
successboolWhether the operation itself succeeded.
processmapProcess 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_access is what the operation needs. A read needs FILE_READ_DATA; a write needs FILE_WRITE_DATA; an mmap with PROT_EXEC needs FILE_EXECUTE.
  • matched_access is requested_access intersected with the handle's continuous audit mask. This is why the event exists — the bits that triggered it.
  • granted_access is what the handle was opened with. An operation can only succeed if requested_access is 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.

KeyTypeMeaning
subjectmapSubject record (§2.1).
object_contextbin or nilObject identifier from the access check.
privilegestringThe canonical privilege name.
requested_accessuintThe bits the caller requested that this privilege might address.
granted_accessuintThe bits the privilege contributed, before later narrowing.
surviving_accessuintThe subset of granted_access that reached the final granted mask.
successboolTrue when surviving_access is non-empty.
processmapProcess 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:

  • SeSecurityPrivilege
  • SeTakeOwnershipPrivilege
  • SeBackupPrivilege
  • SeRestorePrivilege
  • SeRelabelPrivilege

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.

KeyTypeMeaning
subjectmapSubject record (§2.1).
object_contextbin or nilCaller-supplied object identifier.
kindstringsacl-error or staging-mismatch.
phasestringWhich phase of CAAP evaluation produced the diagnostic.
policy_sidbin or nilThe policy involved. nil for staging-mismatch.
rule_indexuint or nilThe rule involved. nil for staging-mismatch.
reasonstring or nilDiagnostic text, for sacl-error.
requested_accessuintThe mask the caller requested.
effective_granted_accessuintTotal granted under the effective policy.
staged_granted_accessuintTotal the staged policy would have granted.
object_results_differboolWhether staged and effective differed for this object.
processmapProcess 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.

KeyTypeMeaning
session_iduintThe destroyed session's LUID.
user_sidbinThe session's user SID.
logon_typeuintInteractive, Network, and so on.
auth_packagestringThe authenticating package — Kerberos, NTLM, local.
created_atuintWhen 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.

KeyTypeMeaning
subjectmapSubject record (§2.1) for the access that triggered detection.
object_contextbin or nilIdentifier of the object whose descriptor was corrupt.
reasonstringWhat was wrong — sd_too_large, acl_malformed, sid_invalid.
processmapProcess 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.

KeyMeaning
pathThe relative path within the mount, /-prefixed.
provider_indexThe stratum the object was copied from.
provider_stratumThat stratum's path.
create_indexThe stratum it was copied into.
create_stratumThat stratum's path.
result_errnoZero 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.

KeyMeaning
pathThe relative path within the mount.
operationThe operation name.
provider_indexThe provider stratum's index.
provider_stratumThat stratum's path.
errnoThe refusal.
deferred flagWhether 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.

EventEmitted when
LCS_KEY_OPEN_AUDITA key open matched a SACL audit ACE.
LCS_BACKUP_STARTBefore REG_IOC_BACKUP reads any subtree data.
LCS_BACKUP_COMPLETEAfter a backup completes, or fails after starting.
LCS_RESTORE_STARTBefore REG_IOC_RESTORE modifies any source state.
LCS_RESTORE_COMPLETEAfter a restore completes, or fails after starting.
LCS_SOURCE_VALIDATION_FAILURELCS rejected malformed source data.
LCS_SELF_CONFIG_INVALIDLCS 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.

KeyMeaning
callerCaller summary (§2.3).
key GUIDThe key that was opened.
requested_accessThe mask after registry generic mapping, with MAXIMUM_ALLOWED re-added if the caller asked for it.
granted_accessThe mask granted. Forced to zero on a denial.
decisionallowed or denied.
sacl_match_flagsBit 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:

GroupClasses
Name fieldsmalformed_layer_name, malformed_key_name, malformed_value_name
Structuralmalformed_response_payload, malformed_key_metadata, malformed_value_payload, malformed_delete_layer_orphan_list
Descriptorsmalformed_security_descriptor, malformed_layer_metadata_security_descriptor
Sequencingfuture_sequence_number, duplicate_winning_sequence_tie
Protocolunknown_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.

OffsetSizeField
04total_len
42event_type
62name_len
8name_lenname, 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.

SizeField
2path_depth
2 + n eachpath_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 #

EventFires whenCarries
job.createdThe job object exists.Job identifier, service name, type, image path, identity, operation identifier.
job.startedexec succeeded.Job identifier, PID, cgroup path.
job.endedThe 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:

EventAdds
operation.requested—
operation.started—
operation.completedduration_ns, result
operation.failedduration_ns, failure_reason
operation.cancelledreason
operation.mergedmerged_into
operation.abortedduration_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_reason on operation.failed
  • reason on operation.cancelled and operation.aborted
  • error in 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.

TypeEmitted forEmitted today
peipkg.installA successful installyes
peipkg.upgradeA successful upgrade, downgrade, or undoyes
peipkg.uninstallA successful uninstallyes
peipkg.refreshA repository refresh, successful or partially failedyes
peipkg.transaction-failedA rejected or rolled-back transactionyes
peipkg.recoveryA recovery resolved through peipkg recoveryes
peipkg.authorisationAn operator authorisation recordyes
peipkg.repo-addA repository addyes
peipkg.repo-removeA repository removeyes
peipkg.claimA claim grant or revokeyes
peipkg.config-changeA trust-policy or transport-flag changeno

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 #

FieldContent
txn_idThe transaction identifier.
outcomesuccess, rejection, or rollback.
repoThe repository, for repository operations.
detailThe rejection reason, the operation count, or the authorised action.
timestampRFC 3339, UTC.
packagesName, 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 recover produces peipkg.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 optional policy — the two decisions most worth recording are the two that are not.
  • peipkg-compose emits 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.

ConditionType
Lost events detected on a CPUsynthetic.gap
eventd started and attached to KMESsynthetic.startup
Graceful shutdown beginningsynthetic.shutdown
A write to any store failedsynthetic.storage_error
A configuration value changed at runtimesynthetic.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 #

TypeEmitterWhere
access-auditKACS§3.1
continuous-auditKACS§3.2
privilege-useKACS§3.3
caap-policy-diagnosticKACS§3.4
logon-session-destroyedKACS§3.5
corrupt-sdKACS§3.6
STRATAFS_COPY_UPStrataFS§4.1
STRATAFS_MUTATION_REFUSEDStrataFS§4.2
LCS_KEY_OPEN_AUDITLCS§5.2
LCS_BACKUP_STARTLCS§5.1
LCS_BACKUP_COMPLETELCS§5.1
LCS_RESTORE_STARTLCS§5.1
LCS_RESTORE_COMPLETELCS§5.1
LCS_SOURCE_VALIDATION_FAILURELCS§5.3
LCS_SELF_CONFIG_INVALIDLCS§5.3
job.createdpeinit§6.1
job.startedpeinit§6.1
job.endedpeinit§6.1
operation.requestedpeinit§6.2
operation.startedpeinit§6.2
operation.completedpeinit§6.2
operation.failedpeinit§6.2
operation.cancelledpeinit§6.2
operation.mergedpeinit§6.2
operation.abortedpeinit§6.2
peipkg.installpeipkg§7.1
peipkg.upgradepeipkg§7.1
peipkg.uninstallpeipkg§7.1
peipkg.refreshpeipkg§7.1
peipkg.transaction-failedpeipkg§7.1
peipkg.recoverypeipkg§7.1
peipkg.authorisationpeipkg§7.1
peipkg.repo-addpeipkg§7.1
peipkg.repo-removepeipkg§7.1
peipkg.claimpeipkg§7.1
peipkg.config-changepeipkg§7.1 — specified, not emitted

A.2 Not KMES #

TypeEmitterTransportWhere
synthetic.gapeventdWritten direct to a shard§8.1
synthetic.startupeventdWritten direct to a shard§8.1
synthetic.shutdowneventdWritten direct to a shard§8.1
synthetic.storage_erroreventdWritten direct to a shard§8.1
synthetic.config_changeeventdWritten direct to a shard§8.1
Watch recordsLCSread() on a key fd§5.4

A.3 Which carry an identity, and how #

EventsIdentity from
KACS, all but logon-session-destroyedsubject record in the payload (§2.1)
logon-session-destroyeduser_sid and session_id directly; the session was the subject
LCS, six of sevencaller summary in the payload (§2.3)
StrataFS, peinit, peipkgThe envelope only. No identity in the payload (§1.2)
eventd syntheticNone. eventd is describing itself

A.4 Known holes #

Collected from the chapters, because a reader planning coverage needs them in one place:

  • peipkg.config-change is 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 optional policy, or peipkg-compose (§7.2).
  • A registry key open requesting MAXIMUM_ALLOWED alone emits no LCS_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-diagnostic with kind = staging-mismatch does not identify which rule differed, and its two masks can be equal while object_results_differ is true (§3.4).
  • There is no logon-session-created, no token-created, and no heartbeat (§3.6).
  • peipkg emits nothing when the caller's token lacks the audit privilege (§7.1).

Peios Learn — documentation for the Peios project.

Built with Trail.