2.2 Event Model
2.2.1 Structure #
An event is a packed binary header followed immediately by its msgpack
payload, written and delivered as a single contiguous byte sequence
with no padding or alignment gaps anywhere. The header is never
represented as a C struct in the kernel; it is serialised field by
field, in order, directly into the ring buffer. The field-by-field
layout — offsets, sizes, and endianness — is part of the consumer
contract and is defined in the PSPK event stream specification. In
summary: all fields before the event type string sit at fixed offsets,
the event type string begins at offset 77 with its u16 length at
offset 75, the header is exactly 77 + type_len bytes, the payload
occupies the bytes from header_size to event_size, and the next
event begins at offset event_size from the start of the current one.
All multi-byte header integers are little-endian; the identity GUIDs
are copied as opaque 16-byte values.
event_size, header_size, and type_len are structural fields
computed by KMES during construction. The emitter supplies only the
event type string and the payload, and KMES copies both verbatim.
2.2.2 Intrinsic stamps #
KMES populates four intrinsic stamp fields at emission time, unconditionally; the emitter cannot supply them.
timestamp— wall clock time (CLOCK_REALTIME, viaktime_get_real_ns()) at the moment KMES accepts the event, in nanoseconds since the Unix epoch.sequence— the emitting CPU's per-boot counter, incremented and then read, so the first event on each CPU gets 1.cpu_id— the CPU on which the ring buffer write occurs, which identifies the per-CPU buffer holding the event.origin_class— for syscall emission, set unconditionally to 0 (userspace); the caller cannot influence it. For kernel emission, the value the calling subsystem passed, written to the header without validation — kernel emitters are trusted to pass an assigned value.
The timestamp is captured before the sequence number is assigned, so two events with the same timestamp on the same CPU are ordered by sequence.
2.2.3 Identity stamps #
The three identity GUIDs are captured by calling KACS accessors during the preemption-disabled ring buffer write phase, after the sequence number is taken and before the header is built. All three accessors are safe with preemption disabled — each is a handful of pointer dereferences and a 16-byte copy, with no allocation and no sleeping. The stamps therefore reflect the thread's identity at the moment of the ring buffer write, not at syscall entry.
kacs_effective_token_guid()reads the thread's subjective credentials (current_cred()), so during impersonation — which installs the impersonation token viaoverride_creds()— it yields the impersonation token's GUID. Otherwise it equals the true token GUID.kacs_primary_token_guid()reads the real credentials (current_real_cred()), which impersonation leaves untouched, so it always yields the process's primary token GUID.kacs_process_guid()reads the process GUID KACS assigned when the process's security state was created at fork. Threads created withCLONE_THREADshare the process state, and no exec path writes the field, so the GUID is stable for the process's lifetime.
All three accessors return the null GUID when there is no task context
(!current or not in_task()), and the token accessors also return it
when the token cannot be resolved. A fully null identity triple
therefore indicates emission before KACS initialisation, from a kernel
thread, or from interrupt or softirq context. No event is ever stamped
with the identity of a task that merely happened to be interrupted.
For batch emission, the timestamp and the three identity GUIDs are captured once, before the per-event loop, and shared by every event in the batch: the batch executes with preemption disabled on one CPU, so the emitting thread's identity cannot change mid-batch. Each event still receives its own sequence number.
2.2.4 Ordering #
Cross-CPU ordering is by timestamp. Events with identical timestamps
from different CPUs were genuinely concurrent and have no defined
relative order. Within one CPU, sequence is the reliable ordering
primitive, monotonic even across wall-clock discontinuities; events
with identical timestamps on the same CPU are ordered by sequence.
2.2.5 Payload #
The payload is a single msgpack value. KMES neither interprets nor modifies it — buffering and delivery are content-blind — and it performs no payload validation at all for kernel emitters, which are trusted callers inside PKM.
Payloads arriving through the syscall interface are validated before acceptance by an iterative (non-recursive) msgpack walker implemented in Rust, operating on the kernel's staged copy of the payload:
- The payload is exactly one well-formed top-level msgpack value.
Trailing bytes after it are rejected, as is the never-used
0xc1type byte. A zero-length payload is rejected — an empty byte sequence is not a msgpack value — so syscall events always carry a payload. (Kernel emitters can emit header-only events withevent_size == header_size.) - Nesting depth is bounded by the configured
MaxNestingDepth(§2.6). Depth counts from 1 at the top-level value; each child of an array or map sits one deeper than its container. A non-empty container at the maximum depth is invalid, because its children would exceed the limit; an empty array or map at the maximum depth is valid, consuming no depth. Map keys and values each occupy a child slot, so a map contributes twice its entry count of children. - The walker's own stack is 256 frames, matching the upper bound of
the
MaxNestingDepthrange; a configured depth outside 1–256 causes every payload to be rejected rather than any to be waved through. - Length prefixes inside msgpack are big-endian, per the MessagePack specification — the one big-endian ingredient in an otherwise little-endian event.
A rejected payload fails the syscall with EINVAL and nothing is
written to the ring buffer.
The event type string is validated as UTF-8 on the syscall path by the same staged-copy pass; kernel emitters' type strings are trusted and copied as given. Types are compared by consumers as raw bytes — no case folding or normalisation is applied anywhere.
2.2.6 Size limits #
Three structural bounds apply to every event, and one configurable policy bound applies to syscall emitters only:
- The event type length fits the header's
u16type_lenfield and is nonzero. Syscall emitters cannot express an overlong type — the ABI length field is alreadyu16— but the kernel emission API takes asize_tand enforces the bound itself. - The total event size (header + payload) fits a
u32, checked with overflow-safe arithmetic on the declared lengths. - The total event size does not exceed 50% of the per-CPU ring buffer capacity — a fixed ratio, not configurable, protecting a CPU's event history from a single giant event. An event of exactly half the capacity is accepted. Capacity is always a power of two, so the halving is exact.
- Syscall events are additionally bounded by the registry-configurable
MaxEventSize(§2.6), rejected withENOSPCwhen exceeded. Kernel emitters are exempt from this policy limit.