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

Events

Single-page view · as markdown

Events overview

Peios / Developing for Peios / Events

KMES is Peios's event system, and it is the sole event path on the system. Audit records, subsystem events, and your own application events all travel the same way: the kernel stamps each event with trusted metadata and writes it into a per-CPU, lock-free ring buffer, and consumers drain those rings. This section teaches both sides — producing and consuming — via event.h and msgpack.h.

What an event is #

An event has two parts:

  1. Kernel-stamped metadata you cannot forge — a CLOCK_REALTIME timestamp, a per-CPU monotonic sequence number, the CPU id, an origin class, and identity GUIDs (the effective token, the true token, and the process). This is the trustworthy skeleton: when you consume an event, you know who emitted it and when, because the kernel wrote that, not the emitter.
  2. A payload — a single MessagePack value that you define. This is your event's actual content.

The event type names the kind of event. It's a dotted name made of your package name, what the event is about, and what happened, such as org.example.backup.snapshot.created. Its fields follow one vocabulary shared by every emitter on the system. Naming and shaping your events shows how to choose both.

Payloads are MessagePack, and you own them #

The kernel does not build or interpret payloads — it only structurally validates them on emit (one well-formed MessagePack value, within size and nesting limits). So userspace owns encoding and decoding, and the SDK ships a MessagePack codec whose validator's acceptance is matched to the kernel's check. Build a payload with the writer, and a successful peios_mp_writer_bytes (or peios_mp_validate) means the emit call will accept it.

Per-CPU rings and lost events #

Events live in per-CPU ring buffers — one ring per logical CPU, lock-free so producers never block on consumers. Two consequences shape how you consume:

  • You drain per CPU. To see everything, run a reader per CPU (discover the count by attaching upward from CPU 0 until it fails).
  • Rings can lap. If you don't drain fast enough, new events overwrite old ones you haven't read. The per-CPU sequence numbers are contiguous, so a gap in the sequence means events were lost — and the reader tracks that count for you.

Privileges #

The two sides are gated separately:

  • Emitting requires SeAuditPrivilege.
  • Consuming (attaching to a ring) requires SeSecurityPrivilege.

Two ways to consume #

The SDK offers a high-level reader that hides the entire lock-free drain — barriers, lapping recovery, lost-event accounting, buffer-resize handling, and the wait — behind a simple next/wait loop. That's what almost everyone should use. There is also a low-level ring API for callers who need to drive the drain inside their own event loop. Both are covered in consuming events.

Where to go in this section #

  • Naming and shaping your events — design an event before you emit it: its type, its fields, its tier and its catalogue entry.
  • Emitting events — build a payload and emit, singly or in batches.
  • Consuming events — drain the rings with the high-level reader (and, briefly, the low-level ring).
  • event.h and msgpack.h — the exhaustive reference.

Naming and shaping your events

Peios / Developing for Peios / Events

Every event on Peios follows one naming scheme and one field vocabulary, so that an operator can find your events with the same queries and security descriptors they use for everyone else's. This guide walks through designing one event, from deciding that it should be an event to describing it for the catalogue. The rules themselves are in PGSS Events; this page applies them.

The running example is a backup tool, packaged as org.example.backup, that writes a snapshot archive and wants to record each one.

Decide that it is an event #

Ask two questions, in order:

  1. Does an operator care? If only the tool's developers would read it, write a log line instead.
  2. Does one instance matter on its own? If it does, it's an event. If only the trend matters — snapshots per hour, bytes per day — it's a metric.

An operator wants to know that last night's snapshot failed, so each snapshot is an event. See Events, logs and tiers.

Name the event type #

An event type is your package name, then one or more nouns from general to specific, then a past-tense verb:

org.example.backup.snapshot.created
  • The root is your package name, split at its dots. Short roots such as kacs and peinit are reserved to platform components.
  • Add a noun only where someone would write a policy on the set beneath it. org.example.backup.snapshot groups everything about snapshots, which an administrator might plausibly switch off or grant access to as a whole.
  • End with what happened, in the past tense: created, started, ended, deleted.
  • Use one type for success and failure. A failed snapshot is still org.example.backup.snapshot.created, carrying outcome.success = false. Don't add a snapshot.failed type.
  • Use lowercase and kebab-case within a segment: copied-up, never copied_up or copiedUp.

See Event types for the full grammar.

Choose the fields #

A field is named for the part it plays, then what it is, then which property:

FieldValue in this event
object.kindfile
object.file.pathThe snapshot archive written
object.file.sizeIts size in bytes
operation.durationHow long the snapshot took
outcome.successWhether it was written
outcome.reasonWhy not, when it wasn't
outcome.errnoThe error number, when there is one

Look a field up before you invent one. The standard fields cover almost everything an event carries: who acted (subject.*), what was acted on (object.*), where something came from or went to (source.* and destination.*), and how it turned out (outcome.*). Every defined field is described in /usr/share/evman/, and evman looks them up: evman object.file lists what is defined under object.file, and evman -k size searches.

A few things never go in your payload:

  • Anything the header already carries. The kernel stamps the time, the sequence number and your token and process identities on every event. Don't write a timestamp.
  • A principal's name. Carry its SID in subject.token.sid or similar. Consumers resolve names when they read the record.
  • A field you have no value for. Leave the key out rather than writing nil, "" or 0.

If your event genuinely needs a field the catalogue doesn't have, put it beneath your package name, under the role it describes:

object.org.example.backup.snapshot.id

This should be rare. If you find a field that every backup tool would want, that's a gap in the standard set worth reporting.

Encode the values #

Each field's type fixes its wire form. The ones you'll meet most:

Kind of valueWrite it as
A SIDThe binary SID, never an SDDL string
A GUID16 raw bytes, never a hyphenated string
A timeNanoseconds since the Unix epoch, as an unsigned integer
A durationNanoseconds, as an unsigned integer
A yes or noA MessagePack boolean, never the string "true"
One of a fixed setA kebab-case string: disk-full, not 2 or DISK_FULL

The full table is in Values.

Build the payload #

A dotted field path is a chain of nested maps: object.file.path is {object: {file: {path: …}}}. Never write a key containing a dot; it becomes a different field that no query can reach.

peios_mp_writer *w = peios_mp_writer_new();

peios_mp_write_map(w, 3);                       /* object, operation, outcome */

peios_mp_write_str(w, "object", 6);
peios_mp_write_map(w, 2);                       /* kind, file */
peios_mp_write_str(w, "kind", 4);
peios_mp_write_str(w, "file", 4);
peios_mp_write_str(w, "file", 4);
peios_mp_write_map(w, 2);                       /* path, size */
peios_mp_write_str(w, "path", 4);
peios_mp_write_str(w, path, path_len);
peios_mp_write_str(w, "size", 4);
peios_mp_write_uint(w, size_bytes);

peios_mp_write_str(w, "operation", 9);
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "duration", 8);
peios_mp_write_uint(w, elapsed_ns);

peios_mp_write_str(w, "outcome", 7);
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "success", 7);
peios_mp_write_bool(w, true);

When the snapshot fails, the same type carries the reason instead of the size:

peios_mp_write_str(w, "outcome", 7);
peios_mp_write_map(w, 3);
peios_mp_write_str(w, "success", 7);
peios_mp_write_bool(w, false);
peios_mp_write_str(w, "reason", 6);
peios_mp_write_str(w, "disk-full", 9);
peios_mp_write_str(w, "errno", 5);
peios_mp_write_int(w, -ENOSPC);

outcome.reason is for filtering, so it's a value from a fixed set. If you want to tell a person more, add outcome.detail as free text; nothing parses it. See Outcomes.

Emitting events covers sending the finished payload.

Choose a tier #

Every event type declares how much it matters to an operator:

TierUse it for
standardWhat an administrator reviews: lifecycle, configuration, failures. Most events.
verboseDetail for an active investigation. Off unless switched on.
debugFor your own developers. Off unless switched on.
essentialExtremely important and rare, or already governed by its own configuration. Never switched off.

A snapshot record is standard. Reserve essential for the two reasons in its row; a package submitted to the Peios package repository is checked for it.

Administrators switch event types on and off in the emission policy, under Machine\Generic\Events. Your types live under Machine\Generic\Events\org\example\backup, and an event type that is switched off is never written. See Emission policy.

Describe it in a fragment #

Ship a fragment describing every event type you write and every field you define, installed as /usr/share/evman/org.example.backup.evman:

--- field object.org.example.backup.snapshot.id
type: bin.guid

The snapshot an event concerns, by the GUID the backup tool assigned it.

--- event org.example.backup.snapshot.created
tier: standard

A snapshot was written, or an attempt to write one failed.

field: object.kind                            required
field: object.file.path                       required
  The snapshot archive.
field: object.file.size                       when outcome.success == true
field: object.org.example.backup.snapshot.id  required
field: operation.duration                     required
field: outcome.success                        required
field: outcome.reason                         when outcome.success == false
  values: disk-full | source-unreadable | cancelled
  closed: false
  Why the snapshot was not written.
field: outcome.errno                          optional

You define only your own field. The standard ones are referenced, never redefined: two fragments defining the same field is an error, because one value with two names can be denied under one and read under the other. Writing evman pages covers the format, checking a fragment with evman lint, and installing it; the normative rules are in The catalogue.

If your program guards access to something #

A daemon that decides who may use its own objects doesn't emit its own "access denied" event. It makes the decision through the kernel's access check and passes the object's identity along, so that the kernel's audit records the decision like any other. See Access decisions.

Where to go next #

  • Emitting events: send the payload, singly or in batches.
  • PGSS Events: the rules this page applies, in full.

Emitting events

Peios / Developing for Peios / Events

Emitting an event is two steps: build a MessagePack payload, then hand it and an event type to the kernel. This guide shows both; event.h and msgpack.h are the full references. Emitting requires SeAuditPrivilege.

Choose the event type and fields first: Naming and shaping your events covers that, and the examples below use its backup-tool event.

Ask whether the type is on #

An administrator can switch event types on and off in the registry, and an emitter must not write a type that is switched off (PGSS §6.9). Open a policy handle once, and ask it before building each payload. The tier is the one your fragment declares for the type:

peios_event_policy *policy = peios_event_policy_open();     /* once, at startup */

static const char type[] = "org.example.backup.snapshot.created";
if (peios_event_policy_enabled(policy, type, sizeof type - 1,
                               PEIOS_EVENT_TIER_STANDARD) != 1)
    return;                                                  /* off: build nothing */

The handle caches its answers and watches the registry, so asking costs little. A change the administrator commits applies to your next decision. Essential types are always on. See the emission policy for the details.

Build the payload #

Use the MessagePack writer to encode a single top-level value: a map whose nested maps spell the field paths. This payload carries outcome.success:

peios_mp_writer *w = peios_mp_writer_new();

peios_mp_write_map(w, 1);                              /* {outcome: {success: true}} */
peios_mp_write_str(w, "outcome", 7);
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "success", 7);  peios_mp_write_bool(w, true);

const void *payload;
ssize_t plen = peios_mp_writer_bytes(w, &payload);     /* validates as it borrows */
if (plen < 0) { /* EINVAL: malformed/under-filled — check peios_mp_writer_error */ }

peios_mp_writer_bytes validates that what you built is exactly one well-formed value, so a non-negative return means the payload is emit-ready. (Remember a map of n needs 2*n values — one per key and value.)

Emit it #

static const char type[] = "org.example.backup.snapshot.created";

int rc = peios_event_emit(type, sizeof type - 1, payload, (uint32_t)plen);
peios_mp_writer_free(w);

if (rc != 0) {
    switch (errno) {
    case EPERM:  /* no SeAuditPrivilege */          break;
    case EINVAL: /* zero-length type or bad payload */ break;
    case ENOSPC: /* payload too large */            break;
    case EAGAIN: /* rate-limited — back off */      break;
    }
}

The event type is length-counted UTF-8 and must be non-zero length. It isn't NUL-terminated on the wire, which is why the example passes sizeof type - 1. On success the kernel stamps the trusted metadata (timestamp, sequence, identity GUIDs) and sets origin_class = userspace, which queries see as emitter.class; you don't provide any of that, and your payload must not repeat it.

Validating untrusted payloads first #

If a payload's shape comes from dynamic or untrusted input, validate it in userspace before emitting so you handle the failure on your terms rather than as an EINVAL from the kernel:

if (peios_mp_validate(payload, plen, KMES_CONFIG_MAX_NESTING_DEPTH_DEFAULT) != 0) {
    /* reject it yourself */
}

The validator's acceptance matches the kernel's emit-time check at that depth bound.

Emitting in batches #

A high-rate producer should batch. peios_event_emit_batch emits many events in one call, so a single timestamp capture, identity capture, and consumer wake cover the whole set:

#define EVENT_TYPE(s) s, sizeof(s) - 1

struct peios_event_entry entries[3] = {
    { EVENT_TYPE("org.example.backup.snapshot.created"), pa, pa_len },
    { EVENT_TYPE("org.example.backup.snapshot.created"), pb, pb_len },
    { EVENT_TYPE("org.example.backup.snapshot.deleted"), pc, pc_len },
};

uint32_t emitted = 0;
int rc = peios_event_emit_batch(entries, 3, &emitted);
if (rc != 0) {
    /* errno is from entries[emitted] — the first that failed.
       entries[0..emitted) were emitted; resume from `emitted`. */
}

count must be in [1, KMES_BATCH_MAX_ENTRIES]. On failure, errno is the reason the first failing entry failed and emitted tells you how many succeeded before it, so you know exactly where to resume. One caveat: rate-limiting is all-or-nothing for a batch — an EAGAIN emits none of it, so on EAGAIN back off and retry the whole batch.

Next #

  • Consuming events — the other side of the pipe.
  • msgpack.h reference — the full encoder, including containers, extensions, and raw splicing.

Consuming events

Peios / Developing for Peios / Events

Consuming events means draining the per-CPU ring buffers. The SDK's high-level reader hides all the hard parts, so most consumers are a short loop. This guide shows that loop and how to parse what you read; event.h has the full API. Consuming requires SeSecurityPrivilege.

The reader loop #

Open a reader for a CPU, then loop next/wait:

peios_event_reader *r = peios_event_reader_open(cpu);
if (!r) { /* errno */ }

for (;;) {
    struct peios_event ev;
    int rc = peios_event_reader_next(r, &ev);
    if (rc == 1) {
        handle_event(&ev);                       /* got one */
    } else if (rc == 0) {
        peios_event_reader_wait(r, -1);          /* none right now — sleep */
    } else {
        break;                                   /* error */
    }
}
peios_event_reader_close(r);

peios_event_reader_next returns 1 (event filled), 0 (nothing available — call wait), or -1 (error). peios_event_reader_wait blocks until events arrive or the timeout elapses (negative = forever). The reader handles the memory barriers, lapping recovery, buffer-resize handling, and futex wait internally — you just alternate the two calls.

Parsing an event #

Each struct peios_event gives you the trusted metadata by value and the payload as a MessagePack value. Parse it with a reader:

void handle_event(const struct peios_event *ev)
{
    /* Metadata is trustworthy — the kernel stamped it. */
    /* ev->timestamp, ev->sequence, ev->cpu_id, ev->origin_class,
       ev->process_guid, ev->effective_token_guid, ... */

    /* event_type and payload are NOT NUL-terminated — use the lengths. */
    static const char type[] = "org.example.backup.snapshot.created";

    if (ev->event_type_len == sizeof type - 1 &&
        memcmp(ev->event_type, type, sizeof type - 1) == 0) {

        struct peios_mp_reader mp;
        peios_mp_reader_init(&mp, ev->payload, ev->payload_len);

        ssize_t pairs = peios_mp_read_map(&mp);
        for (ssize_t i = 0; i < pairs; i++) {
            const char *key; ssize_t klen = peios_mp_read_str(&mp, &key);
            /* dispatch on key; read or peios_mp_skip(&mp) the value */
        }
    }
}

The lifetime rule #

ev->event_type and ev->payload point into the ring mapping. They are valid only until your next peios_event_reader_next call, and only while the slot hasn't been overwritten. So copy out anything you need to keep before continuing the loop — don't stash the raw pointers.

Draining every CPU #

Rings are per-CPU, so one reader sees only one CPU's events. To consume the whole machine, run a reader per CPU — typically one thread each. Discover the CPU count by attaching upward until it fails:

uint32_t ncpu = 0;
for (;;) {
    uint64_t cap;
    int fd = peios_event_attach(ncpu, &cap);   /* low-level probe */
    if (fd < 0) break;                          /* EINVAL past the last CPU */
    close(fd);
    ncpu++;
}
/* now spawn one peios_event_reader per cpu in [0, ncpu) */

Watching for loss #

If a consumer falls behind, the producer laps it and events are lost. Poll peios_event_reader_lost to see the cumulative count of lost events (derived from sequence gaps). A rising number means you aren't draining fast enough — process events more cheaply, hand off to a worker, or accept the loss deliberately.

The low-level ring #

If the reader's loop doesn't fit your event model — you want the ring integrated into an existing epoll/state-machine loop, driving the read position yourself — the low-level ring API exposes the mapping directly: peios_event_ring_map, the position accessors (write_pos/tail_pos/generation), peios_event_ring_event_at to parse a slot, and peios_event_ring_wait to sleep. It's more bookkeeping (you own the read position and the empty/lapping/generation checks) for more control. Reach for it only when you need to; the high-level reader is the right default.

Next #

  • event.h reference — the full reader and ring APIs, and every struct peios_event field.
  • Auditing — the operator-side view of the event and audit stream.

Peios Learn — documentation for the Peios project.

Built with Trail.