These docs are under active development.
On this page
reference 6 min read

event.h — Events (KMES)

<peios/event.h> is the client surface of KMESPeios'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_type and payload point 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_open attaches to cpu_id and maps its ring, ready to drain (NULL with errno on failure). peios_event_reader_close tears it down.
  • peios_event_reader_next fetches the next event into out (non-NULL). Returns 1 (event filled), 0 (none available right now — consider wait), or -1 with errno. The out pointers are valid only until the next call.
  • peios_event_reader_wait blocks until events are available or timeout_ms elapses (negative = forever). Returns 1 (call next), 0 (timeout/interrupted), or -1.
  • peios_event_reader_lost returns 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_map maps and validates a ring fd from peios_event_attach; ring must be zeroed or previously unmapped (remapping an active ring fails EBUSY). peios_event_ring_unmap releases it.
  • Positions are free-running byte counters. write_pos is where the producer will write next (acquire-loaded); tail_pos is the oldest still-live byte (advances as the ring laps); an event lives at (read_pos & (capacity - 1)). You drain by walking read_pos from tail_pos toward write_pos. generation changes when the buffer is resized — re-read capacity when it does.
  • peios_event_ring_event_at parses the event at read_pos into out and returns its byte size (advance read_pos by that), or -1 if the slot is corrupt. You must have confirmed read_pos is in [tail_pos, write_pos) first. Pass out == NULL to validate a slot and get its size without borrowing the event_type/payload pointers.
  • Before sleeping, arm the advisory wake flag with peios_event_ring_set_need_wake(ring, 1), then peios_event_ring_wait futex-waits until events past read_pos may be available or timeout_ms elapses (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.