On this page
event.h — Events (KMES)
<peios/event.h> is the client surface of KMES — Peios's sole event path. The kernel stamps every event with trusted metadata (timestamp, per-CPU sequence, CPU id, identity GUIDs) and writes it into a per-CPU lock-free ring buffer. There is no other way to emit or observe events: audit records, subsystem events, and your own application events all flow through the same rings. Producers emit; consumers attach to the rings and drain them.
Each event payload is a single MessagePack value — build and parse it with <peios/msgpack.h>.
Two privileges gate the module: emitting requires SeAuditPrivilege, and consuming (attaching to a ring) requires SeSecurityPrivilege.
Emitting events
int peios_event_emit(const char *event_type, uint16_t event_type_len,
const void *payload, uint32_t payload_len);
Emits a single event. event_type is a length-counted UTF-8 event kind such as "my.app.login" — not NUL-terminated, and its length must be non-zero. payload is payload_len bytes of MessagePack (one well-formed value). The kernel validates the payload (one well-formed MessagePack value within the configured size and nesting limits) and stamps origin_class = userspace. Returns 0, or -1 with errno:
| errno | Cause |
|---|---|
EPERM |
No SeAuditPrivilege. |
EINVAL |
Zero-length type, or a malformed payload. |
ENOSPC |
Payload exceeds the size caps. |
EAGAIN |
Rate-limited. |
EFAULT |
Bad pointer. |
Since the kernel's payload check matches peios_mp_validate, you can validate in userspace first and turn a would-be EINVAL into a check you control.
/* Build a payload, then emit. */
peios_mp_writer *w = peios_mp_writer_new();
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "user", 4); peios_mp_write_str(w, "alice", 5);
const void *buf; ssize_t n = peios_mp_writer_bytes(w, &buf);
if (n >= 0)
peios_event_emit("my.app.login", 12, buf, (uint32_t)n);
peios_mp_writer_free(w);
Batch emit
struct peios_event_entry {
const char *event_type; /* length-counted UTF-8; not NUL-terminated */
uint16_t event_type_len;
const void *payload; /* MessagePack bytes */
uint32_t payload_len;
};
int peios_event_emit_batch(const struct peios_event_entry *entries,
uint32_t count, uint32_t *emitted_out);
peios_event_emit_batch emits several events in one call, amortising the per-call overhead — a single timestamp capture, identity capture, and consumer wake cover the whole batch. count is in [1, KMES_BATCH_MAX_ENTRIES]. It returns 0 if all count were emitted, or -1 with the errno of the first entry that failed, with *emitted_out (if non-NULL) set to how many entries preceded the failure — so you know exactly where to resume. Rate-limiting is all-or-nothing here: an EAGAIN emits none of the batch.
Consuming events
A consumed event is described by struct peios_event. The kernel-stamped header is copied to you by value; the two variable parts point into the ring mapping.
struct peios_event {
uint64_t timestamp; /* ns since the Unix epoch (CLOCK_REALTIME) */
uint64_t sequence; /* per-CPU, per-boot monotonic (gap = lost events) */
uint16_t cpu_id;
uint8_t origin_class; /* 0 = userspace, 1 = KMES, 2 = KACS, 3 = LCS */
uint8_t effective_token_guid[16];
uint8_t true_token_guid[16];
uint8_t process_guid[16];
const char *event_type; /* not NUL-terminated; use event_type_len */
uint16_t event_type_len;
const void *payload; /* a MessagePack value */
uint32_t payload_len;
};
The trusted metadata is the point of KMES: the timestamp, the identity GUIDs (the effective and true tokens, and the process), and the origin_class are stamped by the kernel and cannot be forged by the emitter. sequence is per-CPU, per-boot monotonic — a gap in it means events were lost (overwritten before you drained them).
Lifetime:
event_typeandpayloadpoint into the ring mapping and are valid only until the next read advance, and only while the slot has not been overwritten. Copy out whatever you need before continuing to the next event.
Attaching to a ring
int peios_event_attach(uint32_t cpu_id, uint64_t *capacity_out);
The low-level primitive: attach to CPU cpu_id's ring buffer, returning a fd and writing the data-region capacity to *capacity_out. Discover the CPU count by counting up from 0 until peios_event_attach returns -1 with errno == EINVAL. Requires SeSecurityPrivilege (EPERM otherwise). You then mmap the fd via peios_event_ring_map. Most callers should use the high-level reader instead, which does the attach and mmap for you.
The high-level reader
typedef struct peios_event_reader peios_event_reader;
peios_event_reader *peios_event_reader_open(uint32_t cpu_id);
void peios_event_reader_close(peios_event_reader *r);
int peios_event_reader_next(peios_event_reader *r, struct peios_event *out);
int peios_event_reader_wait(peios_event_reader *r, int timeout_ms);
uint64_t peios_event_reader_lost(const peios_event_reader *r);
The reader owns the attach + mmap and hides the whole lock-free drain — memory barriers, lapping recovery, sequence-gap (lost-event) accounting, buffer resize/generation handling, and the futex wait. You just loop next/wait.
peios_event_reader_openattaches tocpu_idand maps its ring, ready to drain (NULLwitherrnoon failure).peios_event_reader_closetears it down.peios_event_reader_nextfetches the next event intoout(non-NULL). Returns1(event filled),0(none available right now — considerwait), or-1witherrno. Theoutpointers are valid only until the next call.peios_event_reader_waitblocks until events are available ortimeout_mselapses (negative = forever). Returns1(callnext),0(timeout/interrupted), or-1.peios_event_reader_lostreturns the cumulative count of lost events (from sequence gaps) — poll it to monitor whether you're draining fast enough.
The canonical consume loop, per CPU:
peios_event_reader *r = peios_event_reader_open(cpu);
for (;;) {
struct peios_event ev;
int rc = peios_event_reader_next(r, &ev);
if (rc == 1) {
/* handle ev — copy out event_type/payload before the next call */
} else if (rc == 0) {
peios_event_reader_wait(r, -1); /* sleep until more arrive */
} else {
break; /* error */
}
}
peios_event_reader_close(r);
To consume the whole machine, run one reader per CPU (discover the count as above), each typically on its own thread.
The low-level ring
For callers that want to drive the drain themselves — integrating the rings into a custom event loop, say — the ring API exposes the mapping directly. The accessors apply the correct memory barriers; you own the read position and the empty/lapping/generation checks.
struct peios_event_ring { uint64_t _opaque[4]; }; /* opaque */
int peios_event_ring_map(int fd, uint64_t capacity, struct peios_event_ring *ring);
void peios_event_ring_unmap(struct peios_event_ring *ring);
uint64_t peios_event_ring_capacity(const struct peios_event_ring *ring);
uint64_t peios_event_ring_write_pos(const struct peios_event_ring *ring); /* acquire */
uint64_t peios_event_ring_tail_pos(const struct peios_event_ring *ring); /* acquire */
uint64_t peios_event_ring_generation(const struct peios_event_ring *ring);
void peios_event_ring_set_need_wake(const struct peios_event_ring *ring, int set);
ssize_t peios_event_ring_event_at(const struct peios_event_ring *ring,
uint64_t read_pos, struct peios_event *out);
int peios_event_ring_wait(const struct peios_event_ring *ring,
uint64_t read_pos, int timeout_ms);
peios_event_ring_mapmaps and validates a ring fd frompeios_event_attach;ringmust be zeroed or previously unmapped (remapping an active ring failsEBUSY).peios_event_ring_unmapreleases it.- Positions are free-running byte counters.
write_posis where the producer will write next (acquire-loaded);tail_posis the oldest still-live byte (advances as the ring laps); an event lives at(read_pos & (capacity - 1)). You drain by walkingread_posfromtail_postowardwrite_pos.generationchanges when the buffer is resized — re-readcapacitywhen it does. peios_event_ring_event_atparses the event atread_posintooutand returns its byte size (advanceread_posby that), or-1if the slot is corrupt. You must have confirmedread_posis in[tail_pos, write_pos)first. Passout == NULLto validate a slot and get its size without borrowing theevent_type/payloadpointers.- Before sleeping, arm the advisory wake flag with
peios_event_ring_set_need_wake(ring, 1), thenpeios_event_ring_waitfutex-waits until events pastread_posmay be available ortimeout_mselapses (negative = forever):1(drain now),0(timeout/interrupted),-1.
The low-level loop mirrors the high-level one but with the position bookkeeping in your hands:
uint64_t rp = peios_event_ring_tail_pos(&ring);
for (;;) {
uint64_t wp = peios_event_ring_write_pos(&ring);
while (rp < wp) {
struct peios_event ev;
ssize_t sz = peios_event_ring_event_at(&ring, rp, &ev);
if (sz < 0) { /* corrupt slot — resync from tail_pos */ break; }
/* handle ev */
rp += (uint64_t)sz;
}
peios_event_ring_set_need_wake(&ring, 1);
peios_event_ring_wait(&ring, rp, -1);
}
Reach for this only when the high-level reader's loop doesn't fit your event model; for almost everything, peios_event_reader_* is the right tool.
See also
<peios/msgpack.h>— building and parsing the payloads events carry.- Auditing — the operator-side view of the event and audit stream.