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

SDK Reference

Header-by-header reference for the Peios SDK — the conventions every call follows, then security descriptors, tokens, access checks, files, processes, the registry, events, msgpack, and the registry-source interface.

Single-page view · as markdown

1.1 Library conventions

Peios / Developing for Peios / SDK Reference / Library Conventions

libpeios has a small number of conventions that hold across every function in every module. They are deliberately uniform: once you know how one function reports an error or returns a variable-length buffer, you know how all of them do. This page is the one to read slowly. Everything else in this documentation assumes it.

The conventions come in four groups: how results are returned, the two-call buffer protocol, memory ownership (builders and views), and the small stuff (file descriptors and constants).

1.2 How results are returned

Peios / Developing for Peios / SDK Reference / Library Conventions

Every entry point reports success or failure through its return type. There are three return shapes, and the shape tells you how to read the result.

1.2.0.1 int — a file descriptor, or zero #

A function returning int returns either:

  • a file descriptor (a non-negative int), when its job is to open something — a token, a registry key, an event stream; or
  • 0 on success, when it performs an action with no handle to hand back; and
  • -1 on failure, with the reason in errno.
int fd = peios_token_open_self(/* … */);
if (fd < 0) {
    /* errno is set — perror(), strerror(errno), etc. */
}

1.2.0.2 ssize_t — a byte length #

A function returning ssize_t produces a variable-length result — a SID, a serialised security descriptor, a formatted string, a registry value. It returns:

  • the length in bytes of the result on success (>= 0); or
  • -1 on failure, with the reason in errno.

These are the functions that use the two-call buffer protocol below. The returned length is always the full length of the result, which is what makes the protocol work.

For functions that format a string, the returned length excludes the terminating NUL — exactly like snprintf. So a return of 41 means "41 characters plus a NUL"; size your buffer as len + 1.

1.2.0.3 Structured results — out-parameters #

When a call produces more than one value, or a value that isn't naturally a length or an fd, it writes through out-parameters and returns int (0 / -1). The access check is the archetype: it returns 0 when access is granted and -1 with errno == EACCES when it is denied, and it writes the granted access mask through an out-parameter either way.

uint32_t granted = 0;
int rc = peios_access_check(/* … */, &granted);
/* rc == 0: granted; rc == -1 && errno == EACCES: denied.
   `granted` is populated in both cases. */

A denial is a normal, expected outcome, not a bug — which is why it is reported the same disciplined way as any other errno, rather than through a separate channel.

1.2.0.4 errno #

Failure is always reported through the standard C errno. The library sets errno on every -1 return and uses ordinary, portable errno values — there are no libpeios-specific or PKM-specific error numbers to learn. The ones you will see most:

errnoMeaning in libpeios
EINVALMalformed input — a bad SID, an unparseable SDDL string, an argument out of range.
ERANGEYour output buffer was non-zero but too small. Nothing was written. (See the protocol below.)
EACCESAn access check denied the request.
ENOMEMAn allocation failed (for the heap-backed builders).
EBADF, ESRCH, EFAULTThe usual Linux meanings — a bad fd or pidfd, a vanished process, a bad pointer.

Because the values are standard, strerror, perror, and your language's normal errno handling all work unchanged. Check the return value first, then read errno — like any POSIX call, errno is only meaningful after a call that signalled failure.

Nothing ever unwinds across the boundary. The library is compiled to abort rather than propagate a panic through the C ABI, so a call either returns a value you can inspect or the process dies — it never leaves you with a corrupt half-state to reason about.

1.3 The two-call buffer protocol

Peios / Developing for Peios / SDK Reference / Library Conventions

Every function that returns variable-length bytes — anything with an ssize_t return and an (out, cap) pair — follows the same getxattr-style protocol. It is the single most important convention in the library, so it is worth internalising.

The rule:

  • Call with cap == 0 (or a NULL buffer) to probe: the function writes nothing and returns the number of bytes the result needs.
  • Call with a buffer of at least that size to retrieve: the function fills the buffer and returns the number of bytes it wrote.
  • Call with a non-zero but too-small buffer and it fails with ERANGE and writes nothing — never a truncated or partial result.

That last point is the safety property that makes the protocol trustworthy: a too-small buffer is a clean, detectable error, not a silent truncation. You never have to wonder whether you got the whole thing.

The canonical two-call sequence:

/* 1. Probe for the size. */
ssize_t need = peios_sid_format(sid, sid_len, NULL, 0);
if (need < 0) { /* errno set */ }

/* 2. Allocate. For a string, add 1 for the NUL. */
char *buf = malloc(need + 1);

/* 3. Retrieve. */
ssize_t n = peios_sid_format(sid, sid_len, buf, need + 1);
if (n < 0) { /* errno set */ }
/* buf now holds the formatted SID; n is its length (excluding the NUL). */

When you already know a comfortable upper bound, you can skip the probe and call once with a big-enough buffer. Some results have a fixed maximum the library gives you a constant for — for example a SID is never larger than PEIOS_SID_MAX_BYTES, so a stack buffer of that size always fits and never needs a probe. Those shortcuts are called out where they apply; the two-call protocol is always available as the general fallback.

1.4 Memory ownership

Peios / Developing for Peios / SDK Reference / Library Conventions

libpeios never hands you an allocation to free(). Instead it uses two ownership patterns — builders for constructing byte buffers and views for reading them — and both keep the memory question simple: you own your buffers, the library borrows or copies, and the two never get confused.

1.4.0.1 Builders — constructing buffers #

Anything you assemble (an ACL, a security descriptor, a token specification) is built with a builder: an opaque, heap-backed object you create, feed, take the bytes from, and free.

Builders have three properties worth knowing up front:

  1. They are sticky-error. The incremental add/set calls return void — they never fail inline. If one hits a problem (a bad input, an allocation failure), the builder latches the error and every later call is a no-op. You do not have to check each step. Instead you check once, at the end: either call the builder's _error() accessor (it returns the latched errno, or 0 if all is well), or notice that taking the bytes fails. This lets you write a long, clean sequence of add calls without a conditional after every line.

  2. You free every builder you create. Each _new() is paired with a _free(). Builders also have a _reset() that drops the accumulated content and clears the sticky error, so you can reuse one builder across several objects instead of churning allocations.

  3. Taking the bytes: borrow (and sometimes copy). Every builder has a _bytes() that hands back a pointer into the builder — zero-copy, no allocation. That pointer is valid only until the next mutating call, _reset(), or _free() on that builder. Use it when you are going to consume the bytes immediately (for instance, pass them straight into a kernel call). The call comes in two shapes, and not every builder offers a copying counterpart:

    • The security builders (peios_acl_builder_bytes, peios_sd_builder_bytes) return the pointer — NULL if the sticky error is set — and write the length through an optional len_out pointer. Each is paired with a _finish() that copies the buffer into a caller-supplied buffer using the two-call protocol above, for when the bytes must outlive the builder.
    • peios_token_builder_bytes and peios_mp_writer_bytes are shaped the other way round: they return the length as an ssize_t (-1 with errno on a latched error) and write the borrowed pointer through an out-parameter (which may be NULL to get just the length). Neither has a _finish() — copy the borrowed bytes yourself if they need to outlive the builder.

A typical builder lifecycle:

peios_acl_builder *b = peios_acl_builder_new();   /* NULL on OOM */
peios_acl_builder_allow(b, sid, sid_len, mask, 0); /* void — no check */
peios_acl_builder_deny(b, other, other_len, mask, 0);

size_t len;
const void *acl = peios_acl_builder_bytes(b, &len); /* NULL if errored */
if (!acl) { int err = peios_acl_builder_error(b); /* handle */ }
/* … use `acl` before the next mutation … */

peios_acl_builder_free(b);

1.4.0.2 Views — reading buffers #

Anything you parse (a security descriptor, an ACL, a SID array from a token) is read through a view: a small, caller-allocated struct that you point at a buffer you already hold.

Views have their own two rules:

  1. You allocate the view; it is stack-friendly. A view type such as peios_sd_view is an opaque fixed-size struct — you declare one as a local variable and pass its address to the parse call. No heap, no free. The struct's fields are opaque: never read them directly; use the accessor functions.

  2. A view borrows the buffer it parses — zero-copy. The parse call does not copy the data; the view points into your buffer, and every accessor that yields a SID, a nested ACL, or a blob hands back a pointer into that same buffer. So the buffer must stay alive and unmodified for as long as the view — and anything you derived from it — is in use. Free or mutate the underlying buffer and every pointer the view gave you dangles.

peios_sd_view sd;                        /* on the stack */
if (peios_sd_parse(buf, buf_len, &sd) != 0) { /* EINVAL */ }

const void *owner; size_t owner_len;
if (peios_sd_view_owner(&sd, &owner, &owner_len) == 0) {
    /* `owner` points INTO `buf` — valid only while `buf` lives. */
}

Views compose: parsing a security descriptor gives you a peios_sd_view, from which you obtain a peios_acl_view for its DACL, from which you obtain each peios_ace_view. Every one of them borrows the same original buffer, so keeping that one buffer alive keeps the whole tree valid.

The symmetry is the thing to remember: builders own heap and must be freed; views own nothing and borrow your buffer. Constructing is builders, reading is views, and neither ever asks you to free something the library allocated.

1.5 File descriptors

Peios / Developing for Peios / SDK Reference / Library Conventions

Handles that libpeios opens — tokens, registry keys, event streams — are raw int file descriptors, the same kind open() gives you. You close them with close(), poll them, and pass them across exec (or not) with the usual fd machinery.

They are created O_CLOEXEC by default: a handle does not leak across an exec unless you deliberately clear the flag with fcntl. This is the safe default for security-sensitive handles — a token or key fd will not silently end up in a child process you launch.

1.6 Constants

Peios / Developing for Peios / SDK Reference / Library Conventions

libpeios does not invent its own names for the kernel's wire constants. The access-right bits, ACE types, control flags, and mapping structs all come straight from the <pkm/*.h> UAPI headers, and you use those published names directly: KACS_ACCESS_*, KACS_ACE_TYPE_*, KACS_SD_*, struct kacs_generic_mapping, and so on. There is no parallel PEIOS_* aliasing to translate in your head — the name in the PSD, the name in the kernel header, and the name you write in your code are the same name.

The handful of constants that are libpeios's own — buffer-size ceilings like PEIOS_SID_MAX_BYTES, and enums for convenience selectors like enum peios_wks (well-known SIDs) — are prefixed PEIOS_ and documented with the module that defines them.

1.7 The conventions at a glance

Peios / Developing for Peios / SDK Reference / Library Conventions

ConventionThe rule
int returnfd or 0 on success; -1 + errno on failure.
ssize_t returnbyte length on success; -1 + errno on failure. Strings exclude the NUL.
Two-call protocolcap == 0 / NULL probes for the size; too-small non-zero buffer → ERANGE, nothing written.
errnostandard values only; check the return first, then errno.
Access denial-1 + EACCES, with the granted mask still written to the out-param.
Buildersheap-backed, sticky-error, void adders; check _error() at the end; _free() every one; _bytes() borrows, and the security builders add a _finish() that copies.
Viewscaller-allocated (stack), opaque, borrow the parsed buffer; keep that buffer alive and unmodified.
File descriptorsraw int, O_CLOEXEC by default, closed with close().
Constantsuse the <pkm/*.h> KACS_* names directly; only libpeios's own additions are PEIOS_*.

With these in hand, the module documentation reads as just "what does this function do?" — the how of memory and errors is answered here, once, for all of them. Next: your first program, which puts the protocol and the error model to work in something you can compile.

2.1 security.h — Security descriptors

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

<peios/security.h> is the shared vocabulary of the whole access-control surface. SIDs, security descriptors, ACLs, and ACEs are the currency every KACS interface trades in — tokens carry them, files are protected by them, access checks evaluate them, and the registry secures keys with them. They cross the kernel boundary as variable-length, self-relative byte buffers in the MS-DTYP wire formats, and this module is the one place libpeios lifts that raw wire form into something safe to handle from C.

Everything here assumes the library conventions: ssize_t returns are byte lengths using the two-call protocol, builders are heap-backed and sticky-error, and views borrow the buffer they parse. This page does not repeat those rules per function — read that page first.

The module has four parts:

  • SIDs — build, parse, format, and compare security identifiers.
  • ACLs and security descriptors — assemble them with builders.
  • Parsing — read them back with zero-copy views.
  • SDDL and inheritance — the text form and the userspace-only inheritance helpers.

The wire constants (KACS_SID_*, KACS_SD_*, KACS_ACE_*, and struct kacs_generic_mapping) come straight from <pkm/sid.h> and <pkm/sd.h>. libpeios does not re-alias them — you use the published ABI names directly.

2.1.1 See also #

  • Library conventions — the error, buffer, builder, and view rules this page builds on.
  • SIDs and Security descriptors — the operator-side concepts behind this vocabulary.
  • <peios/token.h>, <peios/file.h>, <peios/access.h> — the KACS interfaces that consume this vocabulary, including the generic-mapping tables peios_access_map_generic expects.

2.2 SIDs

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

A SID (Security Identifier) is the unique binary name of a principal. For the full account of what a SID is — its string and binary forms, the mixed endianness, the equality rule — see the operator-side page on SIDs. This section is the API for handling them.

A SID is small and bounded. The largest possible encoding is PEIOS_SID_MAX_BYTES (68) bytes, so a buffer of that size holds any valid SID and the SID builders below never need a two-call probe — you can always pass a PEIOS_SID_MAX_BYTES stack buffer and skip straight to the retrieve call.

#define PEIOS_SID_MAX_BYTES 68u

2.2.0.1 Constructing SIDs #

Each of these encodes a SID into your buffer and returns its length (or -1 with errno). Because a SID fits in PEIOS_SID_MAX_BYTES, the probe is optional — but these are still ssize_t/two-call functions, so passing cap == 0 to probe works too.

FunctionBuilds
peios_sid_build(out, cap, id_authority, sub_auths, count)An arbitrary SID from its parts: a 48-bit identifier authority (numeric, encoded big-endian) and count sub-authorities (encoded little-endian). count is 0..KACS_SID_MAX_SUB_AUTHORITIES.
peios_sid_parse_string(out, cap, sddl)A binary SID from its SDDL string form ("S-1-5-21-…").
peios_sid_integrity(out, cap, level_rid)An integrity-label SID S-1-16-<rid> (see peios_integrity_level).
peios_sid_logon(out, cap, session_id)A logon SID S-1-5-5-<hi>-<lo> from a 64-bit session id.
peios_sid_well_known(out, cap, which)A well-known SID selected by enum peios_wks.
ssize_t peios_sid_build(void *out, size_t cap, uint64_t id_authority,
                        const uint32_t *sub_auths, unsigned count);
ssize_t peios_sid_parse_string(void *out, size_t cap, const char *sddl);
ssize_t peios_sid_integrity(void *out, size_t cap, uint32_t level_rid);
ssize_t peios_sid_logon(void *out, size_t cap, uint64_t session_id);
ssize_t peios_sid_well_known(void *out, size_t cap, enum peios_wks which);

peios_sid_build fails with EINVAL if count exceeds the maximum, and (like all of these) with ERANGE if a non-zero cap is too small.

2.2.0.2 Formatting and inspecting SIDs #

FunctionReturns
peios_sid_format(sid, len, out, cap)The SDDL string form ("S-1-…"), as a string length excluding the NUL — allocate len + 1.
peios_sid_valid(sid, len)true if sid is a structurally valid SID of exactly len bytes.
peios_sid_length(sid)The encoded length of sid, read from its sub-authority count. You must have already validated sid, or bounded it to PEIOS_SID_MAX_BYTES — this trusts the buffer.
peios_sid_equal(a, alen, b, blen)true for exact binary equality — the only equality KACS defines for SIDs.
peios_sid_rid(sid, len)The RID (last sub-authority), or 0 if the SID has none.
ssize_t  peios_sid_format(const void *sid, size_t len, char *out, size_t cap);
bool     peios_sid_valid(const void *sid, size_t len);
size_t   peios_sid_length(const void *sid);
bool     peios_sid_equal(const void *a, size_t alen, const void *b, size_t blen);
uint32_t peios_sid_rid(const void *sid, size_t len);

The split between peios_sid_valid and peios_sid_length is deliberate: validation is the safe check that bounds an untrusted buffer; peios_sid_length is the fast reader you use after you trust the bytes (or when you have already capped the buffer at PEIOS_SID_MAX_BYTES). When in doubt, validate first.

2.2.0.3 Well-known SIDs #

peios_sid_well_known constructs any of the standard system principals without you memorising their numbers:

enum peios_wks {
    PEIOS_WKS_NULL,                 /* S-1-0-0    Nobody */
    PEIOS_WKS_EVERYONE,             /* S-1-1-0    World */
    PEIOS_WKS_LOCAL,                /* S-1-2-0    Local */
    PEIOS_WKS_CREATOR_OWNER,        /* S-1-3-0 */
    PEIOS_WKS_CREATOR_GROUP,        /* S-1-3-1 */
    PEIOS_WKS_OWNER_RIGHTS,         /* S-1-3-4    suppresses owner WRITE_DAC */
    PEIOS_WKS_ANONYMOUS,            /* S-1-5-7 */
    PEIOS_WKS_SELF,                 /* S-1-5-10   PRINCIPAL_SELF */
    PEIOS_WKS_AUTHENTICATED_USERS,  /* S-1-5-11 */
    PEIOS_WKS_SYSTEM,               /* S-1-5-18   Local System */
    PEIOS_WKS_LOCAL_SERVICE,        /* S-1-5-19 */
    PEIOS_WKS_NETWORK_SERVICE,      /* S-1-5-20 */
    PEIOS_WKS_ADMINISTRATORS,       /* S-1-5-32-544 */
};

For the meaning of each principal, see Well-known principals.

2.2.0.4 Integrity levels #

Integrity-label SIDs have the form S-1-16-<rid>, where the RID names a level. peios_sid_integrity takes that RID; the standard levels are:

enum peios_integrity_level {
    PEIOS_IL_UNTRUSTED = 0,
    PEIOS_IL_LOW       = 4096,
    PEIOS_IL_MEDIUM    = 8192,
    PEIOS_IL_HIGH      = 12288,
    PEIOS_IL_SYSTEM    = 16384,
};

These are the labels that appear in a SACL as a SYSTEM_MANDATORY_LABEL ACE (see peios_acl_builder_label).

2.3 Access masks

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

An access mask is a 32-bit set of rights. Masks may contain four generic bits (KACS_ACCESS_GENERIC_READ/WRITE/EXECUTE/ALL) that stand in for object-specific rights until they are mapped to a concrete object class.

uint32_t peios_access_map_generic(uint32_t mask,
                                  const struct kacs_generic_mapping *m);

peios_access_map_generic folds the generic bits of mask into object-specific rights using the mapping m, and clears the generic bits from the result. Each object class publishes its canonical mapping as a data symbol you pass here — peios_file_generic_mapping (from <peios/file.h>) and peios_token_generic_mapping (from <peios/token.h>). Use it when you have a mask written in generic terms (say, from an SDDL string using GR/GW) and need the concrete rights for a specific object type.

2.4 Building ACLs

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

An ACL is an ordered list of ACEs. You assemble one with a peios_acl_builder — create it, add ACEs, take the serialised bytes, free it. Builders follow the sticky-error rules: the adders return void, the first error latches, and you check peios_acl_builder_error at the end.

typedef struct peios_acl_builder peios_acl_builder;

peios_acl_builder *peios_acl_builder_new(void);   /* NULL on OOM */
void               peios_acl_builder_free(peios_acl_builder *b);
void               peios_acl_builder_reset(peios_acl_builder *b);

peios_acl_builder_reset drops every accumulated ACE and clears the sticky error, so you can reuse one builder for several ACLs.

2.4.0.1 Adding ACEs #

The common single-SID families have convenience adders. flags is a mask of KACS_ACE_FLAG_* and is usually 0 — the flags carry inheritance semantics, which matter only for container/inheritable ACEs.

void peios_acl_builder_allow(peios_acl_builder *b, const void *sid, size_t len,
                             uint32_t mask, uint8_t flags);
void peios_acl_builder_deny (peios_acl_builder *b, const void *sid, size_t len,
                             uint32_t mask, uint8_t flags);
void peios_acl_builder_audit(peios_acl_builder *b, const void *sid, size_t len,
                             uint32_t mask, uint8_t flags);
AdderAppends
_allowAn ACCESS_ALLOWED ACE — grants mask to sid.
_denyAn ACCESS_DENIED ACE — denies mask to sid. Order matters: put denies before allows.
_auditA SYSTEM_AUDIT ACE — logs access by sid matching mask. Belongs in a SACL, not a DACL.

For an integrity label there is a dedicated adder:

void peios_acl_builder_label(peios_acl_builder *b, uint32_t integrity_rid,
                             uint32_t policy_mask);

It appends a SYSTEM_MANDATORY_LABEL ACE for integrity level S-1-16-<integrity_rid>. policy_mask is a mask of the KACS_SYSTEM_MANDATORY_LABEL_NO_{READ,WRITE,EXECUTE}_UP bits (from <pkm/sd.h>) that says which accesses a lower-integrity caller is denied. Like _audit, a label ACE belongs in a SACL.

For everything else — object ACEs, callback ACEs, resource-attribute ACEs — there is the general adder and a fully-specified ACE struct:

struct peios_ace_spec {
    uint8_t       type;      /* KACS_ACE_TYPE_* */
    uint8_t       flags;     /* KACS_ACE_FLAG_* */
    uint32_t      mask;
    const void   *sid;       /* trustee */
    size_t        sid_len;
    const uint8_t *object_type;            /* 16-byte GUID, or NULL */
    const uint8_t *inherited_object_type;  /* 16-byte GUID, or NULL */
    const void   *app_data;  /* trailing callback/resource data */
    size_t        app_data_len;
};

void peios_acl_builder_add(peios_acl_builder *b, const struct peios_ace_spec *ace);

Fill in only the fields the type uses; leave the rest NULL/0:

  • Object ACEs (KACS_ACE_TYPE_*_OBJECT) read object_type and inherited_object_type — each a 16-byte GUID, or NULL when absent.
  • Callback and resource-attribute ACEs carry trailing app_data (which is NULL only when app_data_len is 0). For callback ACEs this is the conditional-expression bytecode you can produce with peios_sddl_parse_condition.

The convenience adders are exactly peios_acl_builder_add with a pre-filled spec for the common cases; reach for _add when you need object, callback, or resource-attribute ACEs.

2.4.0.2 Taking the ACL bytes #

const void *peios_acl_builder_bytes(peios_acl_builder *b, size_t *len_out);
ssize_t     peios_acl_builder_finish(peios_acl_builder *b, void *buf, size_t cap);
int         peios_acl_builder_error(const peios_acl_builder *b);
  • peios_acl_builder_bytes borrows: it returns a pointer into the builder (valid until the next mutation, _reset, or _free), writing the length to len_out if non-NULL. It returns NULL if the sticky error is set.
  • peios_acl_builder_finish copies the serialised ACL out using the two-call protocol.
  • peios_acl_builder_error returns the latched errno, or 0 if the builder is healthy.

The usual next step is to hand these bytes to peios_sd_builder_dacl or _sacl.

2.5 Building security descriptors

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

A security descriptor binds an owner, a group, a DACL, a SACL, and control flags into one self-relative buffer. Its builder mirrors the ACL builder's shape.

typedef struct peios_sd_builder peios_sd_builder;

peios_sd_builder *peios_sd_builder_new(void);
void              peios_sd_builder_free(peios_sd_builder *b);
void              peios_sd_builder_reset(peios_sd_builder *b);

2.5.0.1 Setting components #

void peios_sd_builder_owner(peios_sd_builder *b, const void *sid, size_t len);
void peios_sd_builder_group(peios_sd_builder *b, const void *sid, size_t len);
void peios_sd_builder_control(peios_sd_builder *b, uint16_t set, uint16_t clear);
void peios_sd_builder_dacl(peios_sd_builder *b, const void *acl, size_t len);
void peios_sd_builder_dacl_null(peios_sd_builder *b);
void peios_sd_builder_sacl(peios_sd_builder *b, const void *acl, size_t len);
  • Owner / group. Omit the call to leave the component absent. That is exactly what you want when building a partial SD to set only some components via kacs_set_sd — the SD then carries only what you set.
  • Control bits. peios_sd_builder_control sets the bits in set and clears those in clear (KACS_SD_DACL_PROTECTED, and friends). You do not manage SELF_RELATIVE or the *_PRESENT bits — the builder maintains those for you as you add components.
  • DACL / SACL. Pass ACL bytes, typically straight from peios_acl_builder_bytes. An ACL with zero ACEs is a present-but-empty DACL, which grants only the owner's implicit rights.

The DACL has one subtlety worth stating plainly. KACS has no NULL-DACL encoding — there is no "DACL present, pointer null" form; the kernel's parser rejects it. So "grant everyone everything" is expressed as an absent DACL (the DACL_PRESENT control bit clear). peios_sd_builder_dacl_null requests exactly that: it clears any DACL you set earlier and produces the same bytes as never setting a DACL at all. It exists so you can state the grant-all intent explicitly rather than by omission — but be clear that it means grant all, not deny all.

2.5.0.2 Taking the SD bytes #

Identical in shape to the ACL builder:

const void *peios_sd_builder_bytes(peios_sd_builder *b, size_t *len_out);
ssize_t     peios_sd_builder_finish(peios_sd_builder *b, void *buf, size_t cap);
int         peios_sd_builder_error(const peios_sd_builder *b);

_bytes borrows (valid until the next mutation/reset/free, NULL if errored), _finish copies out getxattr-style, _error returns the latched errno.

2.6 Parsing — views

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

To read a security descriptor, ACL, or ACE you use zero-copy views. A view is a caller-allocated, opaque, stack-friendly struct that borrows the buffer you parse — see the view rules. Every accessor that yields a SID, a nested ACL, or a blob returns a pointer into the original buffer, so that buffer must outlive the view and everything derived from it.

typedef struct peios_sd_view        { uint64_t _opaque[8]; } peios_sd_view;
typedef struct peios_acl_view       { uint64_t _opaque[4]; } peios_acl_view;
typedef struct peios_ace_view       { uint64_t _opaque[4]; } peios_ace_view;
typedef struct peios_sid_array_view { uint64_t _opaque[4]; } peios_sid_array_view;

The _opaque arrays are sized for stack allocation with headroom — declare a view as a local and never read its fields.

2.6.0.1 Security-descriptor views #

int      peios_sd_parse(const void *sd, size_t len, peios_sd_view *out);
uint16_t peios_sd_view_control(const peios_sd_view *v);
int      peios_sd_view_owner(const peios_sd_view *v, const void **sid, size_t *len);
int      peios_sd_view_group(const peios_sd_view *v, const void **sid, size_t *len);
int      peios_sd_view_dacl(const peios_sd_view *v, peios_acl_view *out);
int      peios_sd_view_sacl(const peios_sd_view *v, peios_acl_view *out);

peios_sd_parse validates a self-relative SD and populates out, returning 0 or -1 (EINVAL). peios_sd_view_control returns the raw control-bit word.

The four component accessors return 0 with their out-params set on success, or -1 if the component is absent. For the DACL and SACL, -1 also covers the NULL-DACL case — since an absent DACL and a NULL DACL are the same thing in KACS, a -1 from peios_sd_view_dacl uniformly means "no DACL constrains this object."

2.6.0.2 ACL and ACE views #

You can also parse a bare ACL directly — a token's default DACL, for instance, arrives as an ACL, not wrapped in an SD:

int      peios_acl_parse(const void *acl, size_t len, peios_acl_view *out);
unsigned peios_acl_view_count(const peios_acl_view *a);
int      peios_acl_view_ace(const peios_acl_view *a, unsigned i, peios_ace_view *out);

peios_acl_view_count gives the number of ACEs; peios_acl_view_ace populates out for ACE i (0-based, in stored order), returning 0 or -1 (ERANGE for an out-of-range index). Iterate in the obvious way:

unsigned n = peios_acl_view_count(&dacl);
for (unsigned i = 0; i < n; i++) {
    peios_ace_view ace;
    peios_acl_view_ace(&dacl, i, &ace);
    /* inspect ace … */
}

Each ACE is read through its own accessors:

uint8_t  peios_ace_view_type(const peios_ace_view *e);
uint8_t  peios_ace_view_flags(const peios_ace_view *e);
uint32_t peios_ace_view_mask(const peios_ace_view *e);
int      peios_ace_view_sid(const peios_ace_view *e, const void **sid, size_t *len);
int      peios_ace_view_object_type(const peios_ace_view *e, const uint8_t **guid16);
int      peios_ace_view_inherited_object_type(const peios_ace_view *e,
                                              const uint8_t **guid16);
int      peios_ace_view_app_data(const peios_ace_view *e, const void **data,
                                 size_t *len);
AccessorYields
_type / _flags / _maskThe ACE's KACS_ACE_TYPE_* type, KACS_ACE_FLAG_* flags, and 32-bit access mask.
_sidThe trustee SID (a pointer into the buffer). 0 / -1.
_object_typeThe object GUID of an object ACE — 0 with *guid16 set to the 16 bytes, or -1 if not present / not an object ACE.
_inherited_object_typeThe inherited-object GUID, same convention.
_app_dataTrailing application data of a callback or resource-attribute ACE — for a callback ACE, this is the conditional-expression bytecode you can render with peios_sddl_format_condition.

2.6.0.3 SID-and-attributes arrays #

Several token classes — GROUPS, RESTRICTED_SIDS, DEVICE_GROUPS, CAPABILITIES — return a packed [count][sid_len][sid][attrs]… blob rather than an ACL. Parse those with the SID-array view:

int      peios_sid_array_parse(const void *blob, size_t len, peios_sid_array_view *out);
unsigned peios_sid_array_count(const peios_sid_array_view *a);
int      peios_sid_array_get(const peios_sid_array_view *a, unsigned i,
                             const void **sid, size_t *len, uint32_t *attrs);

peios_sid_array_get yields the i-th entry's SID (a pointer into the blob), its length, and its 32-bit attribute word (the KACS_SE_GROUP_* flags — enabled, mandatory, deny-only, and so on).

2.7 SDDL text codec

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

The SDDL codec converts between the binary wire forms above and their human-readable SDDL text (MS-DTYP §2.5.1). This is a pure-userspace facility — the kernel speaks only binary — so it lives entirely in libpeios. All four entries use the two-call protocol (cap == 0 to probe) and fail with EINVAL on malformed input.

ssize_t peios_sddl_parse_sd(void *out, size_t cap, const char *sddl);
ssize_t peios_sddl_format_sd(char *out, size_t cap, const void *sd, size_t sd_len);
  • peios_sddl_parse_sd parses SDDL text (e.g. "O:SYG:BAD:(A;;FA;;;BA)") into self-relative SD wire bytes.
  • peios_sddl_format_sd renders SD wire bytes back to a NUL-terminated SDDL string (length excludes the NUL, so allocate len + 1).

These are the friendliest way to construct a descriptor when you have one written down — parse the string rather than assembling ACEs by hand — and the friendliest way to log or display one.

2.7.0.1 Conditional expressions #

Callback ACEs carry a conditional expression as compiled "artx" bytecode. The codec converts between that bytecode and its SDDL expression text:

ssize_t peios_sddl_parse_condition(void *out, size_t cap, const char *expr);
ssize_t peios_sddl_format_condition(char *out, size_t cap, const void *artx, size_t len);
  • peios_sddl_parse_condition compiles an expression such as @User.Title == "PM" into the bytecode you place in a callback ACE's app_data.
  • peios_sddl_format_condition renders bytecode back to text (with no outer parentheses), length excluding the NUL.

So the round trip for a conditional ACE is: write the condition as text → peios_sddl_parse_condition → put the bytecode in peios_ace_spec.app_data with a callback ACE type → add it to an ACL builder.

2.8 SD inheritance

Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors

Inheritance — computing a child object's ACEs from its parent's inheritable ones — is also pure userspace (MS-DTYP §2.5.3.4). Both helpers take and produce self-relative SDs and use the two-call protocol.

ssize_t peios_sd_reinherit(void *out, size_t cap, const void *parent_sd,
                           size_t parent_len, const void *child_sd,
                           size_t child_len, int is_container);
ssize_t peios_sd_strip_inherited(void *out, size_t cap, const void *sd,
                                 size_t sd_len, uint32_t info);

peios_sd_reinherit recomputes a child SD's inherited ACEs from its parent. It strips the ACEs carrying ACE_FLAG_INHERITED from the child DACL, re-derives them from the parent DACL, and appends them after the child's explicit ACEs; the child's owner, group, SACL, and control bits pass through unchanged. is_container is non-zero if the child is itself a container (which determines how container-inherit and object-inherit flags propagate). This is what you call when a parent's ACL changed and you need to push the new inheritance down to a child.

peios_sd_strip_inherited drops the ACE_FLAG_INHERITED ACEs from the ACLs selected by info — a mask of *_SECURITY_INFORMATION bits, of which DACL_SECURITY_INFORMATION and SACL_SECURITY_INFORMATION are honoured and the rest ignored (selecting neither copies the input verbatim). Owner, group, and control bits pass through. Use it to reduce a descriptor to just its explicit ACEs — for example before storing a "protected" descriptor that should not carry inherited entries.

Both return the new SD's byte length, or -1 with EINVAL (malformed input) or ERANGE (a non-zero buffer too small).

3.1 token.h — Tokens and sessions

Peios / Developing for Peios / SDK Reference / token.h — Tokens

<peios/token.h> is the token surface of KACS. A token is the runtime object that carries an identity — a user SID, group SIDs, privileges, an integrity level, claims — and every access decision is made against one. This module lets you open the tokens that already exist (your own, another process's, a socket peer's), mint new ones, read their contents, transform them, and install or impersonate them.

A token handle is a file descriptor. Every open/create/duplicate call returns a raw int fd, O_CLOEXEC by default, that you close with close(). The access argument several calls take is the desired handle-right mask (KACS_TOKEN_*), access-checked against the token's own security descriptor and cached on the fd — a handle only lets you do what its rights allow.

The wire constants (KACS_TOKEN_*, KACS_IMLEVEL_*, KACS_SE_*_PRIVILEGE, KACS_TOKEN_CLASS_*, KACS_LOGON_TYPE_*) and the ioctl arg structs (kacs_priv_entry, kacs_group_entry) come from <pkm/token.h>. Query payloads that are SID arrays or ACLs are read with the views in <peios/security.h>.

The module divides into: opening & creating, the token-spec builder, query, adjust/transform, and logon sessions.

3.1.1 See also #

  • <peios/security.h> — the SID/ACL/SD vocabulary and the views used to parse group and privilege query payloads.
  • <peios/access.h> — checking access with a token fd.
  • Tokens and Impersonation — the operator-side model.

3.2 Opening and creating tokens

Peios / Developing for Peios / SDK Reference / token.h — Tokens

Each of these returns a token fd (or -1 with errno).

int peios_token_open_self(unsigned flags, uint32_t access);
int peios_token_open_process(int pidfd, uint32_t access);
int peios_token_open_thread(int pidfd, int tid, uint32_t access);
int peios_token_open_peer(int conn_fd);
int peios_token_create_raw(const void *spec, size_t len);
FunctionOpens
peios_token_open_selfThe calling thread's token. flags may be KACS_TOKEN_OPEN_REAL to get the primary token even while the thread is impersonating; otherwise you get the effective (impersonation-aware) token. access is the desired handle rights.
peios_token_open_processThe primary token of the process named by pidfd. Subject to a process-query access check and PIP dominance over the target.
peios_token_open_threadThread tid's impersonation token if it is impersonating, else the process primary token.
peios_token_open_peerThe peer-identity token captured at connect() on a connected Unix stream/seqpacket socket conn_fd — how a server learns who is on the other end of a socket. Reads the KACS_SO_PEER_TOKEN socket option (level SOL_KACS, <pkm/socket.h>). The handle carries fixed `QUERY
peios_token_create_rawMints a token from a pre-built token-spec buffer. This is the escape hatch — prefer the builder below. Requires SeCreateTokenPrivilege.

Errors, per call:

  • peios_token_open_self — EINVAL (unknown flags; empty or unknown access bits), EACCES (the token's own SD denies access).
  • peios_token_open_process — EACCES (any of the three checks failed — process-query right, PIP dominance, or the token SD; deliberately indistinguishable), EBADF (invalid pidfd), ESRCH (target exited), EINVAL (empty or unknown access bits).
  • peios_token_open_thread — the _open_process set, plus ESRCH (thread exited, or not in pidfd's process) and EINVAL (tid <= 0).
  • peios_token_open_peer — ENOTCONN (not connected), ENODATA (connected but no captured identity — a socketpair end), EOPNOTSUPP (a datagram or non-Unix socket), ENOTSOCK (not a socket), EBADF (invalid fd).
  • peios_token_create_raw — EPERM (privilege missing), EINVAL (spec failed kernel validation), EFAULT (bad spec pointer), ENOMEM (allocation failed).

peios_token_open_peer is the cornerstone of local authentication: accept a connection, open the peer token, and you have the caller's identity to query or impersonate — no password, no handshake, just the kernel's word for who connected.

3.2.1 Peer identity on sockets #

int peios_token_impersonate_peer(int conn_fd);
int peios_socket_set_impersonation_level(int sock_fd, uint32_t level);
int peios_socket_get_impersonation_level(int sock_fd, uint32_t *level);
int peios_socket_set_pass_token(int sock_fd, bool on);
int peios_socket_restamp(int sock_fd);
  • peios_token_impersonate_peer is peios_token_open_peer followed by peios_token_impersonate and a close — the one-call form for a handler that impersonates, works, and reverts on a single thread. It returns 0 or -1; the errno is whichever step failed.
  • peios_socket_set_impersonation_level is the client's side of the exchange: it sets the KACS_SO_IMPERSONATION_LEVEL socket option (KACS_IMLEVEL_*), bounding the level at which the peer may capture this socket's identity — Identification to be known but not acted as, Anonymous to withhold identity entirely. Usually set before connect(); it may change at any time and bounds every capture from then on. The default is KACS_IMLEVEL_IMPERSONATION. Errors: EINVAL (unknown level), EOPNOTSUPP (a non-Unix socket).
  • peios_socket_get_impersonation_level reads the level back into *level.
  • peios_socket_restamp replaces the identity a listening socket conveys to connecting clients — captured when listen() was called — with the caller's own (KACS_SO_RESTAMP; EINVAL if the socket is not listening). Capture is symmetric: a client's peios_token_open_peer on its own socket right after connect() returns the listener's identity at Identification level, which is how a client verifies the service it reached. A service that receives listeners it did not create (from the descriptor store after a restart, or from a broker) restamps them before accepting.
  • peios_socket_set_pass_token turns on per-message identity for a sending socket (KACS_SO_PASS_TOKEN): every send then carries the sender's effective identity as a KACS_SCM_TOKEN ancillary message, and the peer's peios_token_open_peer follows whoever was impersonating at each send. This is what makes a pooled or multiplexed connection convey the right identity per request without the client library knowing. To attach a specific token to one message instead — a router forwarding for many clients, or a submitter handing a job manager the identity a job should run as — use peios_socket_send_message below.

3.2.2 Per-message identity and descriptors #

A message on a Unix socket can carry two things beside its bytes: a token the kernel attests the sender could act as (KACS_SCM_TOKEN, level SOL_KACS) and a set of file descriptors (SCM_RIGHTS). Building the control buffer by hand and walking it back is the same twenty lines in every program that does it, and a wrong CMSG_ALIGN is silent; these calls do it once.

struct peios_socket_message {
    int      token_fd;   /* out: attached token (QUERY|IMPERSONATE|DUPLICATE, O_CLOEXEC), or -1 */
    int     *fds;        /* in:  SCM_RIGHTS descriptors land here, each O_CLOEXEC */
    unsigned fd_cap;     /* in:  capacity of fds */
    unsigned fd_count;   /* out: how many were filled */
    unsigned flags;      /* out: PEIOS_SOCKET_MSG_* */
};
#define PEIOS_SOCKET_MSG_TRUNCATED  0x1u   /* data did not fit (MSG_TRUNC) */
#define PEIOS_SOCKET_MSG_CTRUNCATED 0x2u   /* ancillary data did not fit (MSG_CTRUNC) */

ssize_t peios_socket_send_message(int sock_fd, const void *buf, size_t len,
                                  int token_fd, const int *fds, unsigned fd_count,
                                  int flags);
ssize_t peios_socket_recv_message(int sock_fd, void *buf, size_t cap,
                                  struct peios_socket_message *msg, int flags);
int     peios_socket_peer_pidfd(int sock_fd);
  • peios_socket_send_message sends len bytes from buf on sock_fd with one sendmsg(2), attaching token_fd as a KACS_SCM_TOKEN when it is >= 0 and fds[0..fd_count] as SCM_RIGHTS when fd_count > 0; flags are sendmsg flags. Returns the bytes sent. The kernel gates the token attach exactly as it gates impersonating it — the handle needs TOKEN_IMPERSONATE, and the token travels at the sending socket's impersonation level (peios_socket_set_impersonation_level, default Impersonation), which can only lower what the token already carries: if attaching would have to lower the token's level the send fails rather than downgrading. Ownership is unchanged by a send — the caller keeps token_fd and every entry of fds; the kernel duplicates them into the receiver on delivery.
  • peios_socket_recv_message receives one message with recvmsg(2) into buf/cap, with room for one attached token and msg->fd_cap descriptors written into the caller-owned msg->fds (pass NULL with fd_cap == 0 to accept none). MSG_CMSG_CLOEXEC is always added to flags, so every descriptor delivered is O_CLOEXEC. Returns the bytes received (0 at end of stream). On return the caller owns msg->token_fd (when >= 0) and msg->fds[0..fd_count] and closes each; the token handle carries QUERY | IMPERSONATE | DUPLICATE, the same fixed rights as peios_token_open_peer. Nothing leaks on any path: a second token in one message, or a descriptor past fd_cap, is closed by the library before it returns, and msg->flags reports the loss as PEIOS_SOCKET_MSG_CTRUNCATED; PEIOS_SOCKET_MSG_TRUNCATED means the bytes did not fit cap (on a SOCK_SEQPACKET socket the tail is gone). A receiver that cannot accept a truncated message closes what it was handed. On -1 nothing was consumed and *msg is untouched. fd_cap above the kernel's SCM_MAX_FD (253) is capped silently. The truncation flag also covers handles that fit in the syscall's ancillary buffer but exceed the caller's capacity. For example, fd_cap == 0 closes any received rights and reports control truncation. This flag is retained alongside payload truncation when both occur.
  • peios_socket_peer_pidfd returns a new pidfd (O_CLOEXEC) for the process on the other end of a connected Unix socket — getsockopt(SOL_SOCKET, SO_PEERPIDFD), a Linux ≥ 6.5 facility that libpeios only wraps; the option itself comes from the kernel UAPI and glibc. It is the kernel's handle on the peer, which is what peios_token_open_process takes to open the peer's primary token — the identity a process has, as distinct from the identity it conveyed at connect() (peios_token_open_peer) — and never a PID the peer named, so a recycled PID can never be opened by mistake.

Errors: peios_socket_send_message — EINVAL (NULL buffer with len > 0, NULL fds with fd_count > 0, or fd_count above SCM_MAX_FD), EACCES (token_fd lacks TOKEN_IMPERSONATE), EPERM (attaching would lower the token's level), EBADF (a descriptor in fds is not open), plus the sendmsg set (ENOTCONN, EPIPE, EMSGSIZE, EAGAIN). peios_socket_recv_message — EINVAL (NULL msg), plus the recvmsg set (ENOTCONN, EAGAIN, EBADF). peios_socket_peer_pidfd — ENODATA (no connected peer to name), ENOPROTOOPT (a kernel without SO_PEERPIDFD), ENOTSOCK, EBADF, EMFILE.

The client side of a job submission (PSPU §7) is the archetype — one record, one identity, two descriptors:

const char *request = "{\"command\":\"submit\",\"image_path\":\"/usr/bin/backup\","
                      "\"descriptors\":[\"control\"],\"output\":true}";
int fds[2] = { control_sock, STDOUT_FILENO };   /* named descriptors first, output sink last */

/* `job_token` is a token I hold with TOKEN_IMPERSONATE; the job runs as it. */
if (peios_socket_send_message(jobs_sock, request, strlen(request),
                              job_token, fds, 2, 0) < 0) { /* errno */ }
/* I still own job_token, control_sock and my stdout. */

and the manager's side, receiving it:

char buf[64 * 1024];
int fds[8];
struct peios_socket_message msg = { .fds = fds, .fd_cap = 8 };

ssize_t n = peios_socket_recv_message(conn, buf, sizeof buf, &msg, 0);
if (n < 0) { /* errno; nothing consumed */ }
if (n == 0) { /* end of stream */ }
if (msg.flags & (PEIOS_SOCKET_MSG_TRUNCATED | PEIOS_SOCKET_MSG_CTRUNCATED)) {
    /* not the message the sender meant: refuse it and close what arrived */
}
if (msg.token_fd >= 0) {
    /* the sender attested to this identity, at its socket's level */
    uint32_t level;
    peios_token_impersonation_level(msg.token_fd, &level);   /* e.g. refuse below Impersonation */
} else {
    /* no identity conveyed: fall back to the peer process's own primary token */
    int pidfd = peios_socket_peer_pidfd(conn);
    int primary = peios_token_open_process(pidfd, KACS_TOKEN_QUERY | KACS_TOKEN_DUPLICATE);
    close(pidfd);
}
/* msg.fds[0..msg.fd_count] and msg.token_fd are mine to use and close */

3.3 The token-spec builder

Peios / Developing for Peios / SDK Reference / token.h — Tokens

Minting a token means assembling a 192-byte-header wire format with many optional sections. The builder is the ergonomic path — typed setters, no hand-packed offsets — and follows the standard sticky-error builder rules: the setters return void, the first error latches, you check peios_token_builder_error at the end, and you _free every builder.

typedef struct peios_token_builder peios_token_builder;

peios_token_builder *peios_token_builder_new(void);
void                 peios_token_builder_free(peios_token_builder *b);
void                 peios_token_builder_reset(peios_token_builder *b);

3.3.0.1 The index convention #

Three fields — the owner, the primary group, and the restrict/deny indices — refer to SIDs by index into the token's own SID list rather than by value. The convention is fixed:

Index 0 is the user SID. Indices 1..N are the 1st..Nth group you added with peios_token_builder_add_group, in order.

So to make the second group the primary group, you set primary_group_index to 2. Do not add the logon SID yourself — the kernel injects it.

3.3.0.2 Core fields #

void peios_token_builder_user(peios_token_builder *b, const void *sid, size_t len);
void peios_token_builder_add_group(peios_token_builder *b, const void *sid,
                                   size_t len, uint32_t attrs);
void peios_token_builder_privileges(peios_token_builder *b, uint64_t present,
                                    uint64_t enabled);
void peios_token_builder_type(peios_token_builder *b, uint8_t type, uint8_t imp_level);
void peios_token_builder_integrity(peios_token_builder *b, uint32_t rid);
void peios_token_builder_session(peios_token_builder *b, uint64_t session_id);
void peios_token_builder_owner_index(peios_token_builder *b, uint32_t index);
void peios_token_builder_primary_group_index(peios_token_builder *b, uint32_t index);
void peios_token_builder_default_dacl(peios_token_builder *b, const void *acl, size_t len);
SetterSets
_userThe user SID (index 0).
_add_groupAppends a group SID with its KACS_SE_GROUP_* attribute word (enabled, mandatory, deny-only, …). Call once per group, in the order you want them indexed.
_privilegesThe privilege bitmasks: present (which privileges the token holds) and enabled (which are on). Bits are KACS_SE_*_PRIVILEGE.
_typeThe token type (KACS_TOKEN_TYPE_* — primary or impersonation) and the impersonation level imp_level (KACS_IMLEVEL_*). Choose the level deliberately: it caps every identity derived from the token, including a primary. CreateToken accepts all four levels; Identification permits peers to identify the process without acting as it, and Anonymous hides its identity from peers. The process still acts as its own user. Older kernels reject primary levels below Impersonation. DuplicateToken retains its separate Impersonation floor for primary results.
_integrityThe integrity level, as the RID of an S-1-16-<rid> label (see peios_integrity_level).
_sessionThe logon session id the token references.
_owner_index / _primary_group_indexWhich SID (by index) is the default owner / primary group.
_default_daclThe default DACL applied to new objects the token creates (ACL bytes, e.g. from a peios_acl_builder).

3.3.0.3 Advanced fields #

These cover the rest of the token-spec and can be left unset. They are marked [adv] in the header for a reason — most tokens need none of them.

void peios_token_builder_mandatory_policy(peios_token_builder *b, uint32_t bits);
void peios_token_builder_projected_ids(peios_token_builder *b, uint32_t uid, uint32_t gid);
void peios_token_builder_expiration(peios_token_builder *b, uint64_t when);
void peios_token_builder_source(peios_token_builder *b, const char name[8],
                                uint64_t source_id);
void peios_token_builder_audit_policy(peios_token_builder *b, uint32_t bits);
void peios_token_builder_add_restricted_sid(peios_token_builder *b, const void *sid,
                                            size_t len, uint32_t attrs);
void peios_token_builder_add_device_group(peios_token_builder *b, const void *sid,
                                          size_t len, uint32_t attrs);
void peios_token_builder_confinement(peios_token_builder *b, const void *sid, size_t len);
void peios_token_builder_supp_gids(peios_token_builder *b, const uint32_t *gids,
                                   unsigned count);
SetterSets
_mandatory_policyThe mandatory-integrity policy bits governing how the integrity label is enforced.
_projected_idsThe POSIX uid/gid this token projects into the Linux-compatibility layer.
_expirationAn absolute expiry time after which the token is no longer valid.
_sourceThe token's source: an 8-byte name and a source_id, recording who issued it (appears in audit).
_audit_policyPer-token audit policy bits.
_add_restricted_sidAppends a restricting SID (a write-restricted / restricted token intersects these against the normal SIDs).
_add_device_groupAppends a device group SID (the device/machine side of a claim-aware token).
_confinementThe confinement/AppContainer package SID that sandboxes the token.
_supp_gidsReplaces the projected supplementary GIDs (pass NULL, 0 to clear).

3.3.0.4 Token flags #

The four boolean token-spec flags are set together, so a designated initialiser reads clearly:

struct peios_token_flags {
    bool write_restricted;
    bool user_deny_only;
    bool isolation_boundary;
    bool confinement_exempt;
};
void peios_token_builder_flags(peios_token_builder *b, const struct peios_token_flags *f);
  • write_restricted — the token's restricting SIDs are checked only for write access.
  • user_deny_only — the user SID is usable for deny ACEs but not to grant access.
  • isolation_boundary — marks an isolation boundary for confinement.
  • confinement_exempt — the token is exempt from confinement checks.

3.3.0.5 Claims #

A claim is a named, typed, multi-valued security attribute — the input to conditional (callback) ACEs. Claims come in user and device flavours; both share the same shape.

struct peios_token_claim_value {
    uint64_t    scalar;   /* INT64 / UINT64 / BOOLEAN (0 or 1) */
    const void *bytes;    /* STRING (UTF-8) / SID / OCTET */
    size_t      len;
};

struct peios_token_claim {
    const char *name;         /* UTF-8; transcoded to UTF-16LE on the wire */
    uint16_t    value_type;   /* KACS_CLAIM_TYPE_* */
    uint32_t    flags;        /* KACS_CLAIM_ATTR_* */
    const struct peios_token_claim_value *values;
    unsigned    value_count;
};

void peios_token_builder_add_user_claim(peios_token_builder *b,
                                        const struct peios_token_claim *claim);
void peios_token_builder_add_device_claim(peios_token_builder *b,
                                          const struct peios_token_claim *claim);

The value_type selects which member of each value carries the data:

value_typeValue member
KACS_CLAIM_TYPE_INT64 / _UINT64 / _BOOLEANscalar (a boolean is 0 or 1).
KACS_CLAIM_TYPE_STRINGbytes/len — a UTF-8 string (transcoded to UTF-16LE on the wire).
KACS_CLAIM_TYPE_SIDbytes/len — a binary SID.
KACS_CLAIM_TYPE_OCTETbytes/len — an opaque blob.

Each claim you add is round-tripped through the kernel's own claim parser before acceptance, so a malformed claim latches EINVAL on the builder immediately — you find out at build time, not at token-create time.

3.3.0.6 LCS registry credentials #

The final optional section grants the token registry-layer powers: which layer scopes it may resolve and which private layers it owns.

struct peios_token_lcs_credentials {
    const uint8_t (*scope_guids)[16];    /* array of 16-byte GUIDs, each non-nil & unique */
    unsigned    scope_count;             /* <= KACS_TOKEN_LCS_MAX_SCOPE_GUIDS */
    const char *const *private_layers;   /* UTF-8 names, 1..255 bytes, no '/' or '\\', unique */
    unsigned    private_layer_count;     /* <= KACS_TOKEN_LCS_MAX_PRIVATE_LAYERS */
};
void peios_token_builder_lcs_credentials(peios_token_builder *b,
                                         const struct peios_token_lcs_credentials *creds);

Setting it replaces any prior credentials; it is emitted as the last token-spec section. See <peios/registry.h> for what layers and scopes mean.

3.3.0.7 Finishing the builder #

ssize_t peios_token_builder_bytes(peios_token_builder *b, const void **out);
int     peios_token_builder_create(peios_token_builder *b);
int     peios_token_builder_error(const peios_token_builder *b);
  • peios_token_builder_bytes returns the serialised length and, if out is non-NULL, writes a pointer into the builder (valid until the next reset/free) through it. Use this if you want the raw token-spec bytes.
  • peios_token_builder_create does it in one step: serialise and mint, returning the new token fd. This is the usual call. It requires SeCreateTokenPrivilege.
  • peios_token_builder_error returns the latched errno, or 0.

Errors: _bytes and _create first surface any latched builder error — EINVAL (malformed field, SID, claim, or index) or ENOMEM (allocation failed). A clean _create then adds the peios_token_create_raw set: EPERM (privilege missing), EINVAL (spec failed kernel validation), ENOMEM.

peios_token_builder *tb = peios_token_builder_new();
peios_token_builder_user(tb, user_sid, user_len);
peios_token_builder_add_group(tb, admins_sid, admins_len, KACS_SE_GROUP_ENABLED);
peios_token_builder_type(tb, KACS_TOKEN_TYPE_PRIMARY, 0);
peios_token_builder_integrity(tb, PEIOS_IL_MEDIUM);
peios_token_builder_session(tb, session_id);

int tok = peios_token_builder_create(tb);       /* -1 on failure */
if (tok < 0) { int e = peios_token_builder_error(tb); /* or errno */ }
peios_token_builder_free(tb);

3.4 Query

Peios / Developing for Peios / SDK Reference / token.h — Tokens

You read a token's contents by information class. The generic reader handles any class getxattr-style; typed convenience wrappers cover the common ones.

ssize_t peios_token_query(int fd, uint32_t info_class, void *buf, size_t cap);
ssize_t peios_token_user(int fd, void *sid_buf, size_t cap);   /* CLASS_USER */
  • peios_token_query reads the class info_class (KACS_TOKEN_CLASS_*) into buf using the two-call protocol. Classes that return SID arrays or ACLs are parsed afterward with the <peios/security.h> views — e.g. read CLASS_GROUPS into a buffer, then peios_sid_array_parse it.
  • peios_token_user is the same two-call read specialised to the user SID (CLASS_USER): probe with sid_buf == NULL, cap == 0, then retrieve.

For the common scalar classes there are typed helpers that write through a mandatory non-NULL out-pointer and return 0 / -1:

struct peios_privilege_set {
    uint64_t present;
    uint64_t enabled;
    uint64_t enabled_by_default;
    uint64_t used;
};

struct peios_token_statistics {
    uint64_t token_id;
    uint64_t auth_id;       /* LogonSession LUID */
    uint64_t modified_id;
    uint32_t token_type;
    uint32_t reserved;
    uint64_t expiration;
};

int peios_token_type(int fd, uint32_t *out);            /* CLASS_TYPE */
int peios_token_impersonation_level(int fd, uint32_t *out); /* CLASS_IMPERSONATION_LEVEL */
int peios_token_interactivity_scope(int fd, uint32_t *out); /* CLASS_INTERACTIVITY_SCOPE */
int peios_token_guid(int fd, uint8_t guid_out[16]);     /* CLASS_GUID */
int peios_token_statistics(int fd, struct peios_token_statistics *out); /* CLASS_STATISTICS */
int peios_token_integrity(int fd, uint32_t *level_rid_out); /* CLASS_INTEGRITY_LEVEL */
int peios_token_privileges(int fd, struct peios_privilege_set *out); /* CLASS_PRIVILEGES */

peios_token_impersonation_level reads the token's impersonation level (KACS_IMLEVEL_*): the ceiling on everything captured from, conveyed by, or duplicated out of it — on a primary token as much as an impersonation one, since the level only ever ratchets down (Kernel TRM §3.5.1). A manager handed a token over a socket reads this before deciding what the token may become; a token below Impersonation cannot be turned into a primary token for a new process.

peios_token_interactivity_scope reads the token's interactive-environment scope. It is not a logon-session identifier. To read the token's uint64_t LogonSession LUID, use peios_token_statistics and inspect auth_id; that helper returns the complete fixed-width statistics record.

peios_token_guid writes the immutable 16-byte token GUID used by KMES. The caller supplies 16 writable bytes; the helper leaves them unchanged on failure. Rust exposes Token::guid() -> Result<[u8; 16]> and Go exposes (*Token).GUID() ([16]byte, error). All return raw bytes in KMES order. They require TOKEN_QUERY; kernels predating KACS_TOKEN_CLASS_GUID return EINVAL. Deploy the supporting kernel before consumers. The statistics record and its LUID remain unchanged.

peios_token_privileges returns all four privilege words at once: which privileges are present, which are enabled, which are enabled_by_default, and which have been used (the audit trail of privilege use).

Errors (all query calls): EACCES (handle lacks QUERY), EINVAL (unknown class), ERANGE (non-probe buffer too small), EFAULT (bad buffer pointer). The typed helpers add EINVAL (NULL out-pointer, or an unexpected payload shape).

3.5 Adjust and transform

Peios / Developing for Peios / SDK Reference / token.h — Tokens

These change a token or derive a new one from it. Deriving calls return a new fd; in-place adjustments return 0 / -1.

3.5.0.1 Privileges and groups #

int peios_token_adjust_privileges(int fd, const struct kacs_priv_entry *entries,
                                  unsigned count, uint64_t *prev_enabled);
int peios_token_reset_privileges(int fd);
int peios_token_adjust_groups(int fd, const struct kacs_group_entry *entries,
                              unsigned count, uint64_t *prev_state);
int peios_token_reset_groups(int fd);
  • peios_token_adjust_privileges enables/disables the privileges named in entries (each a kacs_priv_entry); if prev_enabled is non-NULL it receives the prior enabled mask, so you can restore it later. peios_token_reset_privileges restores enabled := enabled_by_default. Errors: EACCES (handle lacks ADJUST_PRIVILEGES), EINVAL (empty or oversized batch, duplicate entry, enabling an absent privilege, unknown attribute bits), EFAULT (bad entries pointer).
  • peios_token_adjust_groups is the group analogue. prev_state, if non-NULL, points at a caller array of KACS_TOKEN_GROUP_MASK_WORDS uint64_t words that receives the prior enabled bitmask. peios_token_reset_groups restores the default group state. Errors: EACCES (handle lacks ADJUST_GROUPS), EINVAL (mandatory, deny-only, or logon-SID group targeted; duplicate or out-of-range index; empty batch), EFAULT (bad entries pointer).

3.5.0.2 Duplicate and restrict #

int peios_token_duplicate(int fd, uint32_t access, uint8_t type, uint8_t imp_level);

struct peios_token_restrict {
    uint64_t           privs_to_delete;
    const uint32_t    *deny_group_indices;   /* groups demoted to deny-only */
    unsigned           deny_count;
    const void *const *restrict_sids;        /* added restricting SIDs */
    const size_t      *restrict_sid_lens;
    unsigned           restrict_count;
    uint32_t           flags;                /* KACS_TOKEN_RESTRICT_WRITE_RESTRICTED */
};
int peios_token_restrict(int fd, const struct peios_token_restrict *spec);
  • peios_token_duplicate copies the token, returning a new fd with handle rights access, token type (KACS_TOKEN_TYPE_*), and impersonation level imp_level (KACS_IMLEVEL_*). This is how you turn a primary token into an impersonation token, an impersonation token into a primary one for a process launched as that identity, or narrow a handle's rights. imp_level is a ratchet: it may equal or lower the source's level, never raise it, whichever direction the type goes. Errors: EACCES (handle lacks DUPLICATE, or the new token's SD denies access), EINVAL (unknown type/imp_level, a level above the source's, a primary requested below KACS_IMLEVEL_IMPERSONATION, empty or unknown access bits), ENOMEM (allocation failed).
  • peios_token_restrict creates a filtered token — the sandboxing primitive. It can delete privileges (privs_to_delete), demote groups to deny-only (deny_group_indices, by index), add restricting SIDs (restrict_sids/restrict_sid_lens), and set KACS_TOKEN_RESTRICT_WRITE_RESTRICTED. The result is a strictly less-powerful token you can hand to less-trusted code. Errors: EACCES (handle lacks DUPLICATE), EINVAL (duplicate or out-of-range deny index, malformed restricting SID, unknown flags, NULL spec or arrays), ENOMEM (allocation failed).

3.5.0.3 Impersonation and installation #

int peios_token_install(int fd);
int peios_token_impersonate(int fd);
int peios_token_revert(void);
  • peios_token_install makes this primary token the calling process's primary token. Errors: EACCES (handle lacks ASSIGN_PRIMARY, or SeAssignPrimaryTokenPrivilege missing), EINVAL (not a primary token), EAGAIN (thread set changed mid-install — retry), ENOMEM (allocation failed).
  • peios_token_impersonate makes this impersonation token the calling thread's effective identity — subsequent access checks on that thread run as the impersonated identity. Errors: EACCES (handle lacks IMPERSONATE), EINVAL (not an impersonation token), EPERM (restricted→unrestricted same-user — the one hard deny), ENOMEM (allocation failed).
  • peios_token_revert undoes it: it clears the thread's impersonation token so checks run as the thread's real (primary) identity again. It takes no argument and is a no-op (reported as success) if the thread was not impersonating. This is the inverse of peios_token_impersonate — always pair them, ideally with revert in the cleanup path. Errors: none in normal operation.

The archetypal server flow: peios_token_open_peer the caller → peios_token_impersonate it → do the work as them → peios_token_revert. peios_token_impersonate_peer (see Opening and creating tokens) fuses the first two steps and the close.

3.5.0.4 Linked tokens and defaults #

int peios_token_link(int elevated_fd, int filtered_fd, uint64_t logon_session_id);
int peios_token_get_linked(int fd);
int peios_token_adjust_default(int fd, const void *dacl, size_t len,
                               uint16_t owner_index, uint16_t group_index);
int peios_token_set_interactivity_scope(int fd, uint32_t scope);
  • peios_token_link links an elevated + filtered primary-token pair in logon_session_id — the UAC-style split-token model, where a filtered token is the everyday identity and its elevated linked token is available on demand. peios_token_get_linked opens the linked token of fd, returning a new fd. Errors (_link): EACCES (SeTcbPrivilege missing, or either handle lacks DUPLICATE), EINVAL (self-link, role/logon-session/user-SID mismatch, not primary tokens, unknown logon_session_id, or an fd that is not a token fd), EBADF (invalid fd). Errors (_get_linked): EACCES (handle lacks QUERY), ENOENT (not part of a linked pair, or the pair was destroyed), ENOMEM (allocation failed).
  • peios_token_adjust_default replaces the token's default DACL and/or owner/primary-group indices. dacl == NULL leaves the DACL unchanged (and ignores len); dacl != NULL with len == 0 clears it; an index of 0xFFFF leaves that index unchanged. Errors: EACCES (handle lacks ADJUST_DEFAULT), EINVAL (out-of-range index; malformed or oversized DACL), EFAULT (bad DACL pointer).
  • peios_token_set_interactivity_scope changes the token's uint32_t interactive-environment scope (requires SeTcbPrivilege). This is deliberately separate from the token's uint64_t LogonSession LUID (auth_id) and does not move the token between logon sessions. Errors: EACCES (handle lacks ADJUST_INTERACTIVITY_SCOPE, or SeTcbPrivilege missing).

3.6 Logon sessions

Peios / Developing for Peios / SDK Reference / token.h — Tokens

A logon session is the lightweight kernel bookkeeping a token references — the "login" a token belongs to. Creating and destroying them requires SeTcbPrivilege.

struct peios_session_spec {
    uint8_t     logon_type;     /* KACS_LOGON_TYPE_* */
    const char *auth_package;   /* UTF-8; may be "" */
    const void *user_sid;
    size_t      user_sid_len;
};

int peios_session_create(const struct peios_session_spec *spec, uint64_t *id_out);
int peios_session_destroy_empty(uint64_t session_id);
  • peios_session_create creates a logon session of type logon_type (KACS_LOGON_TYPE_* — interactive, network, service, …) for user_sid, attributing it to auth_package. id_out is mandatory and receives the new session id, which you then pass to peios_token_builder_session. Errors: EPERM (SeTcbPrivilege missing), EINVAL (NULL spec, id_out, or field; malformed SID; oversized spec), EFAULT (bad pointer), ENOMEM (allocation failed).
  • peios_session_destroy_empty destroys a session that has no live tokens — it fails rather than orphaning tokens. Clean up sessions only after every token referencing them is closed. Errors: EPERM (SeTcbPrivilege missing), ENOENT (no such session), EBUSY (live tokens, linked-pair state, or in-flight references).

3.6.1 Listing sessions #

struct peios_logon_session {
    uint64_t       logon_session_id;  /* a token's auth_id */
    uint64_t       created_at;        /* seconds since the Unix epoch */
    uint32_t       logon_type;        /* KACS_LOGON_TYPE_* */
    uint32_t       user_sid_len;
    const uint8_t *user_sid;          /* binary SID */
    const char    *auth_package;      /* UTF-8, not NUL-terminated */
    uint32_t       auth_package_len;
    uint32_t       reserved;
};

peios_logon_sessions *peios_logon_sessions_open(void);
int  peios_logon_sessions_next(peios_logon_sessions *s, struct peios_logon_session *out);
void peios_logon_sessions_close(peios_logon_sessions *s);

The kernel's own list of every live logon session, from /sys/kernel/security/kacs/sessions. Reading it needs BUILTIN\Administrators or SYSTEM.

  • peios_logon_sessions_open takes the whole listing at once, so a walk sees one moment. NULL with errno on failure: EACCES (not an administrator), ENOENT (securityfs not mounted), ENOMEM.
  • peios_logon_sessions_next fills out with the next session and returns 1, or 0 when there are no more. The pointers in out stay valid until the next call. -1 with EPROTO means a line it could not read; call again to step past it.
  • peios_logon_sessions_close frees the reader.

A session is listed while anything holds it, which includes a session nothing has used yet, so a listing can hold sessions with no process in them. SYSTEM's session is 999. To find a session's processes, read each process's token's auth_id (TokenStatistics) and match it.

peios_logon_sessions *s = peios_logon_sessions_open();
struct peios_logon_session one;
while (s && peios_logon_sessions_next(s, &one) == 1)
    printf("%llu type %u\n", (unsigned long long)one.logon_session_id, one.logon_type);
peios_logon_sessions_close(s);

3.7 The generic mapping

Peios / Developing for Peios / SDK Reference / token.h — Tokens

extern const struct kacs_generic_mapping peios_token_generic_mapping;

The canonical generic→specific rights mapping for the token object class. Pass it to peios_access_map_generic or as the mapping in a peios_access_request when the object under check is a token.

4.1 access.h — Access checks

Peios / Developing for Peios / SDK Reference / access.h — Access Checks

<peios/access.h> answers the central question of the whole access-control model: may this subject perform this access on this object? You hand it a token, a security descriptor, and a desired access mask, and it runs the full KACS AccessCheck pipeline and tells you whether access is granted and exactly which rights were granted.

Two things are worth saying up front:

  • These calls are advisory. They evaluate, they do not enforce. peios_access_check tells you what the answer would be; enforcement of a real operation always runs inside the kernel against the subject's own process security block. Use these when your code is the resource manager — you hold an object, you have its security descriptor, and you need to make the grant/deny decision yourself.
  • A denial is a normal result, not an error. Per the library conventions, a denied check returns -1 with errno == EACCES, and the granted mask is still written out. Only a genuine failure (a bad token fd, a malformed SD) is an error in the usual sense.

4.1.1 See also #

  • <peios/security.h> — building the security descriptors and reading the generic-mapping tables this check consumes.
  • <peios/token.h> — obtaining the token_fd to check, and peios_token_generic_mapping.
  • Access decisions — the operator-side account of how KACS reaches a grant/deny decision.

4.2 The request

Peios / Developing for Peios / SDK Reference / access.h — Access Checks

Every check is described by a single struct peios_access_request. Only the first block is needed for an ordinary check; everything below the divider is advanced and may be left zero/NULL. For every pointer/length pair, NULL is valid only when the matching length or count is zero.

struct peios_access_request {
    int      token_fd;   /* -1 = the caller's effective token */
    const void *sd;      /* the object's security descriptor (wire bytes) */
    size_t   sd_len;
    uint32_t desired;    /* desired access mask */
    struct kacs_generic_mapping mapping;   /* the object class's mapping */

    /* ---- [adv] ---- */
    const void *self_sid;        /* PRINCIPAL_SELF substitution; NULL to omit */
    size_t   self_sid_len;
    uint32_t privilege_intent;   /* backup/restore intent bits */
    const struct kacs_object_type_entry *object_tree;
    uint32_t object_tree_count;
    const void *local_claims;    /* @Local claim array */
    size_t   local_claims_len;
    uint32_t pip_type;           /* 0 = use the subject's PSB */
    uint32_t pip_trust;
    const void *audit_context;   /* the guarded object, a PGSS §6.7 map */
    size_t   audit_context_len;
};

4.2.0.1 The core fields #

FieldMeaning
token_fdThe subject token to evaluate. -1 means the caller's own effective token — the common case when you are checking access for yourself. Otherwise pass a token fd from <peios/token.h>.
sd / sd_lenThe object's security descriptor, as self-relative wire bytes — typically from a peios_sd_builder or read off the object.
desiredThe access mask you want checked. May contain generic bits; the mapping resolves them.
mappingThe object class's generic mapping (a struct kacs_generic_mapping), so generic rights in desired and in the SD's ACEs fold to the right object-specific bits. Use the class's published table — e.g. peios_file_generic_mapping or peios_token_generic_mapping.

4.2.0.2 The advanced fields #

Leave these zero/NULL unless you need them:

FieldMeaning
self_sid / self_sid_lenThe SID to substitute for PRINCIPAL_SELF (S-1-5-10) in ACEs — the "self" the object belongs to.
privilege_intentBackup/restore intent bits, letting SeBackupPrivilege / SeRestorePrivilege widen the granted mask as they would for a real backup or restore.
object_tree / object_tree_countAn object-type tree for a per-property check (object ACEs with type GUIDs). Mandatory for peios_access_check_list.
local_claims / local_claims_lenAn @Local claim array to evaluate conditional ACEs against, beyond the claims already on the token.
pip_type / pip_trustProcess-integrity-protection trust label to evaluate against; pip_type == 0 uses the subject's own PSB.
audit_context / audit_context_lenThe identity of the object a daemon is guarding, for the audit record of the check: one MessagePack map holding kind and, under a key equal to the kind, the object's identifying fields — {kind: "service", service: {name: "jellyfin"}} — as PGSS §6.7 defines. The kernel accepts only that shape and fails the check with EINVAL otherwise. It copies the map into the kacs.audit.access.checked record as object.kind and object.<kind>.*, and marks the record fields.attestation.userspace, because the values are the caller's claim. Build it with peios_audit_context_encode. Both check calls validate a non-NULL context before the syscall and fail with EINVAL if the kernel would refuse it.

4.3 The check

Peios / Developing for Peios / SDK Reference / access.h — Access Checks

int peios_access_check(const struct peios_access_request *req,
                       uint32_t *granted, struct peios_access_audit *audit);

Runs the full AccessCheck pipeline. Returns:

  • 0 if every right in desired is granted;
  • -1 with errno == EACCES if any desired right is denied;
  • -1 with another errno on a real error (e.g. EBADF for a bad token_fd, EINVAL for a malformed SD).

granted, if non-NULL, always receives the granted access mask — even on denial. This is the useful part: you can request a broad desired and read back exactly which subset was granted, rather than probing one right at a time. audit, if non-NULL, receives the audit outputs.

struct peios_access_request req = {
    .token_fd = -1,                      /* my own effective token */
    .sd = sd_bytes, .sd_len = sd_len,
    .desired = KACS_ACCESS_READ | KACS_ACCESS_WRITE,
    .mapping = peios_file_generic_mapping,
};

uint32_t granted = 0;
int rc = peios_access_check(&req, &granted, NULL);
if (rc == 0) {
    /* both READ and WRITE granted */
} else if (errno == EACCES) {
    /* denied; `granted` shows what WAS allowed (maybe READ only) */
} else {
    /* error: perror("access_check") */
}

libpeios owns the versioned struct kacs_access_check_args under the hood — it sets caller_size and zeroes the reserved fields so the request stays forward-compatible across kernel versions. You only ever fill in the peios_access_request above.

4.4 Audit outputs

Peios / Developing for Peios / SDK Reference / access.h — Access Checks

struct peios_access_audit {
    uint32_t continuous_audit;   /* OR of matching alarm masks */
    int      staging_mismatch;   /* 1 if the staged CAAP result differs */
};

When you pass a non-NULL audit, the check reports:

  • continuous_audit — the OR of the masks of any SYSTEM_ALARM ACEs that matched. An enforcement point that keeps this mask on a handle emits a kacs.audit.handle.used event for each later operation whose required access overlaps it.
  • staging_mismatch — 1 if evaluating the staged central access policy would have produced a different result than the active one. This is the signal you watch when rolling out a central access policy change: a non-zero value means the pending policy would decide this access differently. The kernel records the same difference as a kacs.caap.staging.diverged event.

These are the outputs the check hands back to you. The audit records it writes to the event stream — kacs.audit.access.checked and kacs.audit.privilege.used — go to KMES, not to this struct. They name the object you checked only if the request carried an audit context, the map {kind: "<kind>", "<kind>": {…}}; anything else in that field fails the check with EINVAL.

4.5 The object-type-list variant

Peios / Developing for Peios / SDK Reference / access.h — Access Checks

int peios_access_check_list(const struct peios_access_request *req,
                            struct kacs_node_result *results, uint32_t count);

peios_access_check_list is the AccessCheckByTypeResultList form — a per-node check over an object-type tree, for objects whose properties or property sets carry their own object ACEs (a directory-service-style object, say). It evaluates the whole tree in one call and reports a separate result for each node.

  • req->object_tree / object_tree_count are mandatory here — they describe the tree of kacs_object_type_entry nodes to evaluate.
  • results receives one kacs_node_result per node, in preorder, and count must equal req->object_tree_count.
  • Returns 0 / -1 (EINVAL if count doesn't match, and the usual errors otherwise).

Each kacs_node_result carries that node's granted mask and status, so you can discover, for example, that a caller may read most of an object but not one protected property — in a single check rather than one per property.

4.6 The audit context

Peios / Developing for Peios / SDK Reference / access.h — Access Checks

A daemon that guards objects of its own (a service manager's services, an event store's namespaces) makes each access decision through peios_access_check. The kernel records that decision in a kacs.audit.access.checked event when the descriptor's SACL asks for one, but the kernel cannot tell what the descriptor belongs to. The audit context tells it. It is the request's audit_context field: one MessagePack map holding kind and, under a key equal to the kind, the fields that identify the object.

{kind: "service", service: {name: "jellyfin"}}

The kernel copies it into the record as object.kind and object.service.name, and marks the record fields.attestation.userspace, because the kernel did not observe those values: they are the daemon's claim. It refuses any other shape with EINVAL. PGSS §6.7 is the specification. A component that guards objects must pass one, and each field it names must be defined, as asserted, in that component's catalogue fragment.

4.6.1 Encoding one #

enum peios_audit_value_type {
    PEIOS_AUDIT_STR  = 0,   /* bytes/len: a UTF-8 string, not NUL-terminated */
    PEIOS_AUDIT_UINT = 1,   /* scalar */
    PEIOS_AUDIT_INT  = 2,   /* scalar, read as a two's-complement int64_t */
    PEIOS_AUDIT_BOOL = 3,   /* scalar: 0 or 1 */
    PEIOS_AUDIT_BIN  = 4,   /* bytes/len: binary, e.g. a SID or GUID */
};

struct peios_audit_field {
    const char *key;         /* NUL-terminated; one kebab-case segment */
    uint32_t    value_type;  /* enum peios_audit_value_type */
    uint64_t    scalar;
    const void *bytes;
    size_t      len;
};

ssize_t peios_audit_context_encode(const char *kind,
                                   const struct peios_audit_field *fields,
                                   size_t count, void *buf, size_t cap);

peios_audit_context_encode writes the map for an object of kind kind, identified by count fields, using the two-call protocol: pass cap == 0 to learn the size. With no fields (fields may then be NULL), the context is {kind: <kind>}.

kind and every key must be a single segment of an event name: kebab-case, [a-z][a-z0-9]*(-[a-z0-9]+)*, so service, log-namespace or unit-name. A value is a scalar in one of the event wire forms: a string, an unsigned or signed integer, a boolean, or binary. A SID goes in as PEIOS_AUDIT_BIN with its binary bytes, never as SDDL text. There is no nil: a field with no value is left out of the array.

struct peios_audit_field fields[] = {
    { .key = "name", .value_type = PEIOS_AUDIT_STR,
      .bytes = "jellyfin", .len = strlen("jellyfin") },
};
uint8_t ctx[128];
ssize_t n = peios_audit_context_encode("service", fields, 1, ctx, sizeof ctx);
if (n < 0) { perror("audit context"); return -1; }

struct peios_access_request req = {
    .token_fd = caller_fd,
    .sd = sd_bytes, .sd_len = sd_len,
    .desired = SERVICE_START,
    .mapping = service_mapping,
    .audit_context = ctx, .audit_context_len = (size_t)n,
};
uint32_t granted = 0;
int rc = peios_access_check(&req, &granted, NULL);

The encoder fails with EINVAL when:

  • the kind or a key is not a segment, or a key is NULL;
  • a key appears twice;
  • value_type is not one of the five, or a boolean's scalar is not 0 or 1;
  • a string is not valid UTF-8, or bytes is NULL while len is not zero;
  • the encoded map would be longer than KACS_ACCESS_CHECK_MAX_AUDIT_CONTEXT_LEN (4096 bytes).

The kernel itself allows a nil value, a nested value, or a key that appears twice inside the body. The encoder refuses all three. A field with no value is left out (PGSS §6.5), an identifying field is a scalar, and a repeated key would make object.<kind>.<key> ambiguous.

4.6.2 Validating one built another way #

int peios_audit_context_validate(const void *buf, size_t len);

If you build the map yourself, for example with the msgpack writer, peios_audit_context_validate checks it against the kernel's own rules. It returns 0 if the kernel would accept it, and -1 with EINVAL otherwise, including for NULL or empty input. The rules are:

  • the top level is a map holding a string kind that is one segment, and at most one other key;
  • that key, if present, equals the kind, and its value is a non-empty map whose keys are segments;
  • nothing follows the map;
  • the bytes are well-formed MessagePack (UTF-8 strings, no reserved 0xc1 byte), nested no deeper than 31 levels. That is the event nesting limit less one, because the context lands one level below the record's root;
  • the map is at most 4096 bytes.

peios_access_check and peios_access_check_list apply the same check to any non-NULL audit_context before they make the syscall. A malformed context therefore fails there with EINVAL, without reaching the kernel. A non-NULL pointer with a zero length is refused too, as the kernel refuses it.

4.6.3 From Rust and Go #

The peios crate builds the same map with an AuditContext and hands it to the AccessCheck builder. Field values convert from &str, the integer types, bool, &[u8] and &SidRef:

use peios::access::{AccessCheck, AuditContext};

let ctx = AuditContext::new("service", &[("name", "jellyfin".into())])?;
let decision = AccessCheck::new(&sd, desired, mapping)
    .token(caller.as_fd())
    .audit_context(&ctx)
    .check()?;

AuditContext::new fails with EINVAL for the same reasons as the C encoder. AuditContext::from_bytes adopts a map you encoded yourself, after validating it.

In Go, libp-go's sd package encodes the map for CheckRequest.AuditContext:

ctx, err := sd.EncodeAuditContext("service", sd.AuditString("name", "jellyfin"))
if err != nil {
    return err
}
dec, err := sd.Check(sd.CheckRequest{
    Token:         caller,
    SD:            sdBytes,
    DesiredAccess: serviceStart,
    Mapping:       serviceMapping,
    AuditContext:  ctx,
})

The field constructors are AuditString, AuditUint, AuditInt, AuditBool, AuditBytes and AuditSID. A refusal wraps sd.ErrBadAuditContext. sd.ValidateAuditContext checks a map built by hand, and Check and CheckList run that check on a non-empty AuditContext before the syscall.

5.1 file.h — File security

Peios / Developing for Peios / SDK Reference / file.h — File Security

<peios/file.h> is the file surface of KACS. Where ordinary POSIX open() gives you a file descriptor governed by mode bits, peios_file_open performs a native KACS open — an NtCreateFile-shaped call carrying a desired access mask, a create disposition, create options, and an optional creator security descriptor — and hands back an ordinary Linux file fd whose granted access mask is fixed for the fd's lifetime. Because the grant is baked into the fd, it can be delegated safely by dup, SCM_RIGHTS, or across exec: whoever holds the fd holds exactly the access it was opened with, no more.

Alongside the open, this module reads and writes a file's security descriptor (by path or by fd) and governs how a superblock without native SD storage is treated.

The wire constants (KACS_DISPOSITION_*, KACS_CREATE_OPT_*, KACS_FILE_*, KACS_SECINFO_*, KACS_MOUNT_POLICY_*, KACS_STATUS_*) come from <pkm/file.h> and <pkm/sd.h>. The security descriptors these calls exchange are built and parsed with <peios/security.h>.

5.1.1 See also #

  • <peios/security.h> — building the creator SDs and parsing the SDs these calls return.
  • <peios/access.h> — evaluating a file SD with peios_file_generic_mapping.
  • File access and Mount policies — the operator-side model of native file security.

5.2 Opening a file

Peios / Developing for Peios / SDK Reference / file.h — File Security

Important

The open-interface comparison records unresolved differences and the limited pinned-source findings applied here. Creator-SD rejection, the accepted create-option bits, and native-open versus SD-query no-follow behavior were checked in source; the other differences remain unresolved. No runtime test or historical release boundary is established.

struct peios_open_params {
    uint32_t    desired_access; /* KACS_FILE_* | standard | generic (strict-mode) */
    uint32_t    disposition;    /* KACS_DISPOSITION_* */
    uint32_t    options;        /* KACS_CREATE_OPT_* */
    uint32_t    flags;          /* AT_SYMLINK_NOFOLLOW | KACS_BACKUP_INTENT | KACS_RESTORE_INTENT */
    const void *sd;             /* creator SD on create, else NULL */
    size_t      sd_len;
};

int peios_file_open(int dirfd, const char *path,
                    const struct peios_open_params *p, uint32_t *status_out);

peios_file_open opens path relative to dirfd (the usual *at convention — an absolute path ignores dirfd, and AT_FDCWD means the current directory). It returns a file fd, or -1 with errno.

The parameters:

FieldMeaning
desired_accessThe access mask you are requesting — KACS_FILE_* object rights, standard rights, or (in strict mode) generic bits the file class maps. The granted subset is what the returned fd is fixed at.
dispositionWhat to do about existence: KACS_DISPOSITION_* — open-existing, create-new, open-or-create, supersede, overwrite, and so on. This is the create/open decision open() splits across O_CREAT/O_EXCL/O_TRUNC.
optionsAt kernel 8e0e22de3a59cad506bbbf8873de456e16ad272d, only KACS_CREATE_OPT_DIRECTORY and KACS_CREATE_OPT_DELETE_ON_CLOSE are accepted; other bits fail with EINVAL. No-follow belongs in flags, not options.
flagsAT_SYMLINK_NOFOLLOW, plus the privilege-intent flags KACS_BACKUP_INTENT / KACS_RESTORE_INTENT that let SeBackupPrivilege / SeRestorePrivilege widen the access the open is granted.
sd / sd_lenThe creator security descriptor — the SD to stamp on a newly created file. Pass NULL when opening an existing file (or to let the parent's inheritance decide the new file's SD).

status_out, if non-NULL, receives a KACS_STATUS_* code telling you what happened — whether the file was opened, created, superseded, overwritten. For OPEN_IF with no creator SD (sd = NULL, sd_len = 0), this distinguishes "created a new file" from "opened the existing one" without a separate stat race. Supplying a creator SD changes the existing-file behavior described below.

Errors include EACCES (a requested right denied — strict mode), EEXIST (create-new and the file exists), ENOENT (open-existing and it doesn't), ENOTDIR (directory option, non-directory target), ELOOP (native open with no-follow and a final symlink), EINVAL (MAXIMUM_ALLOWED without a concrete data/execute bit, unsupported create-option bits, malformed creator SD, NULL path/p, sd == NULL with sd_len != 0), and EBADF (bad dirfd).

At the pinned kernel revision, supplying a creator SD with OPEN fails with EOPNOTSUPP, and with OVERWRITE fails with EINVAL. With OPEN_IF or OVERWRITE_IF, it fails with EINVAL when the path resolves to an existing object. The SDK forwards the disposition and SD; it does not silently drop the SD on that branch. See the source findings. Use create-only when an explicit creator SD is required:

struct peios_open_params p = {
    .desired_access = KACS_FILE_READ_DATA | KACS_FILE_WRITE_DATA,
    .disposition    = KACS_DISPOSITION_CREATE,    /* fail if the path exists */
    .options        = 0,
    .sd             = creator_sd, .sd_len = creator_sd_len,
};
uint32_t status = 0;
int fd = peios_file_open(AT_FDCWD, "data.bin", &p, &status);
if (fd < 0) { /* errno */ }
/* On success, status == KACS_STATUS_CREATED. */

To open an existing object, use KACS_DISPOSITION_OPEN with sd = NULL and sd_len = 0. If create-only returns EEXIST, decide whether opening the existing object is appropriate before issuing that separate call. A create/open retry is not an atomic transaction; neither is a stat-then-open check.

At libpeios de0018bdcaed14abb796aeccec9c8387fd30e452, the wrapper copies options to kacs_open_how.create_options and flags to kacs_open_how.flags, forwards the creator SD, zeroes reserved fields, and passes the struct size as a separate syscall argument. This does not make unsupported option bits valid on another kernel.

5.3 Reading and writing a file's security descriptor

Peios / Developing for Peios / SDK Reference / file.h — File Security

A file's SD can be accessed by path or by fd. In both cases secinfo is a mask of KACS_SECINFO_* bits selecting which components (owner, group, DACL, SACL, …) the operation touches — you read or write just the parts you name and leave the rest alone.

The rights required scale with the components you touch (see Managing file security):

Component (KACS_SECINFO_*)Reading needsWriting needs
OWNER / GROUPREAD_CONTROLWRITE_OWNER (plus owner-SID validation)
DACLREAD_CONTROLWRITE_DAC
SACLACCESS_SYSTEM_SECURITYACCESS_SYSTEM_SECURITY
LABELREAD_CONTROLWRITE_OWNER (the label cannot rise above the caller's integrity without SeRelabelPrivilege)

ACCESS_SYSTEM_SECURITY is itself gated by SeSecurityPrivilege; READ_CONTROL and WRITE_DAC are implicitly granted to the owner. SACL and LABEL cannot be combined in one call (EINVAL). The check is all-or-nothing: if any requested component fails its check, the whole call fails.

5.3.0.1 By path #

ssize_t peios_file_get_sd(int dirfd, const char *path, uint32_t secinfo,
                          void *buf, size_t cap, uint32_t at_flags);
int     peios_file_set_sd(int dirfd, const char *path, uint32_t secinfo,
                          const void *sd, size_t len, uint32_t at_flags);
  • peios_file_get_sd reads the secinfo-selected components of path's SD into buf, getxattr-style (two-call protocol — probe with cap == 0, and a too-small non-zero buffer fails ERANGE without truncating). at_flags accepts AT_SYMLINK_NOFOLLOW; at the pinned source below, it queries the terminal symlink's own SD instead of following it. Errors: EACCES (component right missing), EINVAL (SACL + LABEL together; NULL path, or NULL buffer with non-zero cap), ERANGE (non-probe buffer too small), ENOENT (path doesn't exist), ELOOP (a path-resolution failure, not a blanket rejection of a terminal symlink under no-follow).
  • peios_file_set_sd writes the secinfo components of sd onto path, preserving the components you did not select. So to change only the DACL, build an SD with a DACL, pass secinfo = KACS_SECINFO_DACL, and the owner/group/SACL are untouched. Errors: EACCES (component right missing), EPERM (owner-SID validation failed without SeRestorePrivilege; label raised without SeRelabelPrivilege; MANDATORY attribute removed without SeTcbPrivilege), EINVAL (malformed SD, SACL + LABEL together, NULL or zero-length sd), ENOENT, ELOOP.

Important

No-follow has different meanings for these two operations: native open rejects a terminal symlink with ELOOP, while the by-path SD query selects the link itself. This is verified in kernel 8e0e22de3a59cad506bbbf8873de456e16ad272d and its path resolver. The libpeios 0.5.8 query wrapper at de0018bdcaed14abb796aeccec9c8387fd30e452 forwards at_flags; it does not turn no-follow into a link rejection. The link-query test definition is supporting source evidence, not a test run. This query finding does not establish a successful link-SD write: set-security has separate rights, validation and storage requirements. No deployed version or historical release boundary was verified.

5.3.0.2 By fd #

ssize_t peios_fd_get_sd(int fd, uint32_t secinfo, void *buf, size_t cap);
int     peios_fd_set_sd(int fd, uint32_t secinfo, const void *sd, size_t len);

The same operations against the object fd already refers to. The access check they perform depends on the fd type: a normal file fd is checked against its cached granted mask (the one baked in at open), while an O_PATH, pidfd, or token fd triggers a live check. That distinction — cached for the fixed-grant file fd, live for the others — is documented in the Peios Kernel TRM §3.9, FACS; the practical upshot is that a file fd already opened with the right access can get/set its SD without a second path resolution.

The required rights and errors match the by-path calls, minus the path-resolution failures (ENOENT/ELOOP), plus EBADF (bad fd).

5.3.0.3 By System V IPC kind and id #

ssize_t peios_sysv_get_sd(uint32_t kind, int id, uint32_t secinfo,
                          void *buf, size_t cap);
int     peios_sysv_set_sd(uint32_t kind, int id, uint32_t secinfo,
                          const void *sd, size_t len);

System V message queues, shared memory segments and semaphore arrays carry security descriptors too (Peios Kernel TRM §3.11), but have neither a path nor an fd. kind is one of KACS_SD_AT_SYSV_SHM, KACS_SD_AT_SYSV_MSG or KACS_SD_AT_SYSV_SEM from <pkm/ipc.h>, and id is what shmget, msgget or semget returned. The object is looked up in the caller's IPC namespace and the check is always live against its descriptor: READ_CONTROL (or ACCESS_SYSTEM_SECURITY for the SACL) to read, WRITE_DAC / WRITE_OWNER to write, with the same preserve-unselected-components merge as the file calls. Errors as for the by-fd calls, with EINVAL for an id that names nothing and EIDRM for an object that has been removed. The Rust crate exposes the same pair as file::sysv_get_sd / file::sysv_set_sd with a SysvKind enum.

5.4 Mount policy

Peios / Developing for Peios / SDK Reference / file.h — File Security

Not every filesystem can store native security descriptors. The mount policy governs how KACS treats a superblock that has no native SD storage — whether files there get a synthesised SD, a template SD, or are denied. These calls target the superblock the object fd lives on. At the pinned kernel revision below, both require enabled SeManageVolumePrivilege or SeTcbPrivilege, with successful privilege-use marking.

Important

The gate is verified in kernel 8e0e22de3a59cad506bbbf8873de456e16ad272d, for both reads and writes. The libpeios 0.5.8 wrappers at de0018bdcaed14abb796aeccec9c8387fd30e452 pass through to those syscalls; their TCB-only comments do not enforce an additional privilege gate. No runtime test or release boundary is established here. See operator policy-access checks before changing privileges.

struct peios_mount_policy {
    uint32_t    policy;      /* KACS_MOUNT_POLICY_* */
    uint32_t    flags;
    uint32_t    generation;
    const void *template_sd;
    size_t      template_sd_len;
};

int peios_mount_get_policy(int fd, struct peios_mount_policy *out,
                           void *tmpl_buf, size_t tmpl_cap);
int peios_mount_set_policy(int fd, const struct peios_mount_policy *p);
  • peios_mount_get_policy reads the policy for fd's superblock into out. The template SD is returned into your tmpl_buf getxattr-style: on success out->template_sd points into tmpl_buf when that buffer was large enough, or is NULL if the superblock has no template. A NULL template buffer (or tmpl_cap == 0) is valid only when you don't need the template bytes. A too-small template buffer is not an error — the call still succeeds, reports the true length in out->template_sd_len, and leaves out->template_sd NULL so you can size a retry. Errors: EPERM (neither accepted privilege enabled, or privilege-use marking failed), EBADF (bad fd), EINVAL (NULL out, or NULL tmpl_buf with non-zero tmpl_cap), EFAULT (bad buffer pointer), ENOMEM (allocation failed).
  • peios_mount_set_policy installs p as the superblock's policy. policy is a KACS_MOUNT_POLICY_* value; template_sd/template_sd_len supply the template SD when the policy calls for one. flags and generation must be zero on set — the kernel manages the generation counter itself and rejects a non-zero input. Errors: EPERM (neither accepted privilege enabled, or privilege-use marking failed), EINVAL (unknown or unmanaged policy, non-zero flags/generation, malformed or oversized template, NULL template with non-zero length), EOPNOTSUPP (superblock not KACS-managed), EBADF (bad fd), EFAULT (bad pointer).

5.5 The generic mapping

Peios / Developing for Peios / SDK Reference / file.h — File Security

extern const struct kacs_generic_mapping peios_file_generic_mapping;

The canonical generic→specific rights mapping for the file object class. Pass it to peios_access_map_generic, or as the mapping in a peios_access_request when checking access against a file's SD — for example to pre-flight whether a caller could open a file before you actually open it.

6.1 process.h — Process security

Peios / Developing for Peios / SDK Reference / process.h — Process Security

<peios/process.h> is the process-security surface of KACS. Today it is a small module with two jobs: turning on process mitigations — the hardening controls that live on a process's security block (PSB) — and reading a process's PSB back. More process-security surface will land here as it appears.

The mitigation bits are the KACS_MIT_* flags from <pkm/psb.h> (KACS_MIT_WXP through KACS_MIT_SML, with KACS_MIT_ALL as the mask of all valid bits). KACS_MIT_CFI is a legacy alias that expands to KACS_MIT_CFIF | KACS_MIT_CFIB. The full catalogue and semantics are in the Peios Kernel TRM §3.3, the Process Security Block.

6.1.1 Setting mitigations #

int peios_process_set_mitigations(int pidfd, uint32_t mitigations);

Turns on the mitigation bits named in mitigations (a mask of KACS_MIT_*). Returns 0 on success, or -1 with errno.

Three properties define how this call behaves, and each matters:

  • It is one-way. Mitigation bits can only be set, never cleared. Once a protection is on, it stays on for the life of the process. This is deliberate — a mitigation you could turn off is a mitigation an attacker could turn off — so treat each call as a permanent, additive commitment.
  • It targets a process by pidfd. pidfd == -1 targets the calling process, which is the common case: a program hardens itself early in startup. Targeting another process requires PROCESS_SET_INFORMATION on it plus PIP dominance over it — you cannot harden (or interfere with) a process you don't already dominate.
  • It is activation-backed and fails closed. If a requested protection cannot actually be activated, the call fails without mutating anything — you never end up believing a mitigation is on when it isn't. Either every requested bit is activated and the call succeeds, or nothing changes and it returns -1.
/* Harden the current process: enforce W^X and shadow-stack, refuse to
   proceed if either can't be activated. */
if (peios_process_set_mitigations(-1, KACS_MIT_WXP | KACS_MIT_SML) != 0) {
    perror("set_mitigations");
    /* nothing was changed; decide whether to continue unhardened or abort */
}

Because the call is all-or-nothing, request the bits you require together and check the result once: a success means the whole set is active, a failure means none of this call's bits were applied (bits set by earlier successful calls remain on).

6.1.2 Reading a PSB #

struct peios_psb {
    uint32_t pip_type;         /* 0 none, 512 Protected */
    uint32_t pip_trust;        /* 8192 PeiosTcb */
    uint32_t mitigations;      /* committed KACS_MIT_* bits */
    uint8_t  process_guid[16];
};

int peios_process_psb(int pid, struct peios_psb *out);

Reads process pid's PSB, from /proc/<pid>/psb, into *out. pid <= 0 reads your own. Returns 0, or -1 with errno.

  • Your own is always readable.
  • Another process's needs PROCESS_QUERY_LIMITED on its descriptor — which the default process descriptor gives Everyone — and not PIP dominance. A protected process's PIP type and trust are therefore readable when nothing else about it is, which is what lets a tool explain why it cannot see more.

mitigations is the committed set: bits a call asked for but could not activate are not in it, and neither is the KACS_MIT_CFI alias. process_guid identifies the process for its whole life, unlike its pid, and is what events carry.

Errors: ENOENT (no such process, or it has exited), EACCES (refused), EPROTO (a line this library cannot read), EINVAL (NULL out).

struct peios_psb psb;
if (peios_process_psb(pid, &psb) == 0 && psb.pip_type != 0)
    printf("%d is protected (trust %u)\n", pid, psb.pip_trust);

6.1.3 See also #

  • <peios/token.h> — PIP dominance is determined by the subject's token; process targeting other than self depends on it.
  • Process mitigations — the operator-side account of each mitigation and what it defends against.

7.1 registry.h — The registry (LCS)

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

<peios/registry.h> is the client surface of LCS — the Layered Configuration Subsystem, Peios's kernel-mediated registry. LCS is modelled on the Windows registry: a hierarchy of keys (each with an immutable GUID identity and secured by its own KACS security descriptor) holding typed values. Its distinguishing feature is layers: every write is tagged with a precedence-ordered layer, and the effective view of a value resolves to the highest-precedence entry. That is what lets a base configuration, a site overlay, and a machine-local override coexist on one key and resolve deterministically.

This header is the registry client: open keys, read and write values, enumerate, watch, secure, back up, and run transactions. It does not cover the registry source (the storage backend) side — REG_SRC_REGISTER and the RSI framed protocol — which is a separate library, librsi. A client speaks only the syscalls and ioctls here.

Handles are fds. Three calls create file descriptors — peios_reg_open_key, peios_reg_create_key, and peios_reg_begin_transaction; everything else is an operation on a key fd or transaction fd, gated on the access right granted when the key was opened. The wire constants — value types (REG_SZ … REG_QWORD), key access rights (KEY_*), open/create flags, transaction states (REG_TXN_*), watch filters (REG_NOTIFY_*), and security-info bits — come from <pkm/lcs.h>.

7.1.1 See also #

  • <peios/security.h> — building and parsing the SDs that secure keys.
  • Library conventions — the base error and buffer rules the descriptor reads specialise.
  • The registry — the operator-side model of layers, hives, and precedence.

7.2 The buffer convention here

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

Most of libpeios returns variable-length data with an ssize_t and the two-call protocol. The registry's reads use the same idea but express it through descriptor structs rather than a return value, because a single read often fills more than one buffer (a value's data and its layer name, say). The pattern:

  • Each read takes a descriptor struct with *_cap fields (in) and *_len fields (out), plus buffer pointers.
  • On success it returns 0 and writes the actual length into each *_len.
  • If a buffer is too small it returns -1 with errno == ERANGE and writes the required length into the matching *_len — so a zero-capacity buffer probes the size.
  • A NULL buffer is valid only with zero capacity; NULL with a nonzero capacity is EINVAL.
  • For a read with two buffers, ERANGE is returned if either is too small, and both required lengths are reported, so one probe sizes everything.

Everything else follows the usual Linux convention: 0 / -1 + errno.

7.3 Opening and creating keys

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

int peios_reg_open_key(int parent_fd, const char *path, uint32_t desired_access,
                       uint32_t flags);
int peios_reg_create_key(int parent_fd, const char *path, uint32_t desired_access,
                         uint32_t flags, const char *layer, int txn_fd,
                         uint32_t *disposition_out);

Both resolve path (NUL-terminated) against parent_fd — a key fd for a relative path, or < 0 for an absolute path — and return a key fd whose granted access mask is fixed for its lifetime (like a file fd, so it can be delegated). desired_access is the requested KEY_* rights, checked against the key's SD.

  • peios_reg_open_key opens an existing key. flags may be REG_OPEN_LINK to open a symlink key itself rather than following it. Errors: ENOENT, EACCES, EINVAL, ELOOP, ENAMETOOLONG, ETIMEDOUT, EIO, ENOMEM.
  • peios_reg_create_key opens an existing key or creates a new one. flags may combine REG_OPTION_VOLATILE (a key that does not survive reboot) and REG_OPTION_CREATE_LINK (create a symlink key). layer names the target layer to create in (NUL-terminated), or NULL for the base layer. txn_fd enlists the create in a transaction, or -1 to auto-commit. disposition_out, if non-NULL, receives REG_CREATED_NEW or REG_OPENED_EXISTING. Errors add ENOSPC and EPERM (privileged symlink creation) to the set above.

7.4 Values

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

A value is named (length-counted; an empty name is the key's default value), typed (REG_*), and written into a layer. A base-layer target is layer == NULL with layer_len == 0; a non-NULL pointer with a zero length is rejected EINVAL.

7.4.0.1 Reading a value #

struct peios_reg_value {
    uint64_t sequence;  /* out: effective entry's sequence number */
    void    *data;      /* in:  buffer for the value data (NULL to probe) */
    void    *layer;     /* in:  buffer for the layer name (NULL to probe/skip) */
    uint32_t type;      /* out: value type (REG_*) */
    uint32_t data_cap;  /* in */   uint32_t data_len;  /* out: actual/required */
    uint32_t layer_cap; /* in */   uint32_t layer_len; /* out: actual/required */
};

int peios_reg_query_value(int key_fd, const void *name, uint32_t name_len, int txn_fd,
                          struct peios_reg_value *v);

peios_reg_query_value reads the effective value name on key_fd — the winner of the layer precedence resolution. name_len == 0 reads the default value; txn_fd reads within a transaction, or -1 for none. It fills v->data with the value bytes and v->layer with the name of the layer that won, and reports the resolved type and sequence. Pass a NULL layer buffer if you don't care which layer won. Errors: ENOENT (no effective value, or a tombstone masks it), ERANGE, EACCES, EINVAL.

7.4.0.2 Writing, deleting, tombstoning #

int peios_reg_set_value(int key_fd, const void *name, uint32_t name_len, uint32_t type,
                        const void *data, uint32_t data_len, const void *layer,
                        uint32_t layer_len, int txn_fd, uint64_t expected_seq);
int peios_reg_delete_value(int key_fd, const void *name, uint32_t name_len,
                           const void *layer, uint32_t layer_len, int txn_fd);
int peios_reg_blanket_tombstone(int key_fd, const void *layer, uint32_t layer_len,
                                int set, int txn_fd);
  • peios_reg_set_value writes value name of type into a specific layer (NULL/0 = base). type may be REG_TOMBSTONE to place a per-value tombstone that masks lower layers. expected_seq is a compare-and-swap guard: 0 disables it; otherwise the write applies only if the value's current sequence matches, else EAGAIN. This is how you do lost-update-safe read-modify-write — read the sequence from peios_reg_query_value, then set with expected_seq set to it. Errors: EINVAL, EAGAIN, ENOSPC, ENAMETOOLONG, EPERM, EACCES.
  • peios_reg_delete_value removes a layer's entry for name (NULL/0 = base). It is idempotent, and removing a layer's entry lets any lower-layer value re-emerge — deletion is per-layer, not global.
  • peios_reg_blanket_tombstone sets (set != 0) or clears (set == 0) a blanket tombstone on a layer, masking all lower-precedence values of this key on that layer at once — the wholesale version of a per-value tombstone. set must be 0 or 1 (else EINVAL).

7.4.0.3 Enumerating values #

int peios_reg_query_values_batch(int key_fd, int txn_fd, void *buf, uint32_t cap,
                                 uint32_t *len_out, uint32_t *count_out);

struct peios_reg_enum_value {
    void    *name;      /* in:  buffer for the value name (NULL to probe) */
    void    *data;      /* in:  buffer for the value data (NULL to probe) */
    uint32_t type;      /* out */
    uint32_t name_cap;  /* in */  uint32_t name_len;  /* out: actual/required */
    uint32_t data_cap;  /* in */  uint32_t data_len;  /* out: actual/required */
};
int peios_reg_enum_value(int key_fd, uint32_t index, int txn_fd,
                         struct peios_reg_enum_value *v);

Two ways to read every effective value of a key:

  • peios_reg_query_values_batch reads them all into one buf in a single call — the efficient path. Each record is packed little-endian, back to back: [name_len: u32][name][type: u32][data_len: u32][data], for count records. len_out receives the bytes written (or the required size on ERANGE); count_out receives the record count. Both may be NULL.
  • peios_reg_enum_value reads one value at a time by index, dense over the key's tombstone-resolved values — walk from 0 until ENOENT. Use it when you want to process values incrementally rather than buffer them all.

7.5 Subkeys, metadata, and watches

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

7.5.0.1 Enumerating subkeys #

struct peios_reg_subkey {
    void    *name;             /* in:  buffer for the child's name (NULL to probe) */
    uint64_t last_write_time;  /* out: ns since the Unix epoch */
    uint32_t name_cap;         /* in */  uint32_t name_len;    /* out */
    uint32_t subkey_count;     /* out: the child's subkey count */
    uint32_t value_count;      /* out: the child's value count */
};
int peios_reg_enum_subkey(int key_fd, uint32_t index, int txn_fd,
                          struct peios_reg_subkey *v);

peios_reg_enum_subkey reads the child key at index, dense over visible children — walk from 0 until ENOENT. There is no per-child access check during enumeration (you see the names and counts; opening a child still checks its SD).

7.5.0.2 Key metadata #

struct peios_reg_key_info {
    void    *name;                 /* in:  buffer for the key's leaf name (NULL to probe) */
    uint64_t last_write_time;      /* out */
    uint64_t hive_generation;      /* out: per-hive change epoch */
    uint32_t name_cap;  uint32_t name_len;
    uint32_t subkey_count;         /* out */
    uint32_t value_count;          /* out */
    uint32_t max_subkey_name_len;  /* out */
    uint32_t max_value_name_len;   /* out */
    uint32_t max_value_data_size;  /* out */
    uint32_t sd_size;              /* out: security-descriptor size */
    uint8_t  volatile_key;         /* out: 1 if volatile */
    uint8_t  symlink;              /* out: 1 if a symlink */
};
int peios_reg_query_key_info(int key_fd, struct peios_reg_key_info *v);

peios_reg_query_key_info reads the key's leaf name and its metadata (needs READ_CONTROL). Note the ordering wrinkle: the kernel reports the metadata only once the name fits, so a too-small (or zero-capacity) name buffer returns ERANGE with the required name_len and no metadata — size the name buffer from that, then call again to get everything. The max_* fields are sizing hints for enumerations; hive_generation is a per-hive change epoch you can watch to detect that anything under the hive changed.

7.5.0.3 Deleting and hiding keys #

int peios_reg_delete_key(int key_fd, const void *layer, uint32_t layer_len, int txn_fd);
int peios_reg_hide_key(int key_fd, const void *layer, uint32_t layer_len, int txn_fd);

Both need DELETE access, take a layer (NULL/0 = base) and an optional txn_fd, and cannot target a hive root (EINVAL).

  • peios_reg_delete_key removes this key's path entry in a layer; lower-layer entries re-emerge. It fails with ENOTEMPTY if the key has visible children.
  • peios_reg_hide_key creates a HIDDEN path entry that masks the key in a layer; removing that layer makes the key reappear. This is the key-level analogue of a tombstone — hide rather than destroy.

7.5.0.4 Watching for changes #

int peios_reg_notify(int key_fd, uint32_t filter, int subtree);
int peios_reg_flush(int key_fd);
  • peios_reg_notify arms change watches on key_fd (needs KEY_NOTIFY). filter is a mask of REG_NOTIFY_VALUE / REG_NOTIFY_SUBKEY / REG_NOTIFY_SD (or REG_NOTIFY_ALL); subtree (0/1) extends the watch to descendants. filter == 0 disarms. Once armed, the key fd itself becomes pollable — EPOLLIN signals pending events, and read() on the fd returns the change records. So a watch integrates directly into an epoll loop with no side channel. Errors: ENOENT (orphaned key), EINVAL, EACCES.
  • peios_reg_flush forces the source to persist this key's hive's pending writes (needs KEY_SET_VALUE) and returns once persistence is confirmed — the durability barrier.

The change records. A read() on an armed key fd returns as many complete records as fit in your buffer — records are never split across reads. If the buffer is too small for even the next record the read fails EINVAL (so size it generously — a few KiB), and a non-blocking fd with nothing pending fails EAGAIN. Each record is a little-endian, possibly unaligned byte stream (Peios Kernel TRM §5.6, Watches, with the header offsets in §5.A):

OffsetSizeFieldMeaning
04total_lenRecord size in bytes — advance by this to the next record (future versions may append fields).
42event_typeREG_WATCH_VALUE_SET / _VALUE_DELETED / _SUBKEY_CREATED / _SUBKEY_DELETED / _SD_CHANGED / _KEY_DELETED / _OVERFLOW.
62name_lenByte length of name; 0 for the no-name events (SD_CHANGED, KEY_DELETED, OVERFLOW).
8name_lennameThe changed value or subkey name (UTF-8, not NUL-terminated).

A subtree watch appends two further fields after name: path_depth (u16) and that many length-prefixed path components (u16 length + UTF-8 bytes), locating the changed key relative to the watched key — depth 0 means the watched key itself.

Delivery is best-effort with an overflow fallback: if records accumulate faster than you read them, the oldest are dropped and a REG_WATCH_OVERFLOW record is queued — on seeing one, re-read the watched key (and subtree) to recover current state rather than trusting the stream. Records describe effective (layer-resolved) changes, and uncommitted transactions produce none — events fire at commit.

7.6 Key security descriptors

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

int peios_reg_get_security(int key_fd, uint32_t security_info, void *sd, uint32_t cap,
                           uint32_t *sd_len_out);
int peios_reg_set_security(int key_fd, uint32_t security_info, const void *sd,
                           uint32_t sd_len, int txn_fd);

Keys are KACS-secured, so their SDs are read and written with the same <peios/security.h> vocabulary as files and tokens; security_info selects components (owner/group/DACL/SACL).

  • peios_reg_get_security reads the selected components into sd (KACS binary form), writing the length to *sd_len_out (may be NULL); a too-small buffer returns ERANGE with the required size there, and a zero cap probes. Owner/group/DACL need READ_CONTROL; the SACL needs ACCESS_SYSTEM_SECURITY.
  • peios_reg_set_security applies the selected components of sd, merging with the rest (the kernel parses and validates). The DACL needs WRITE_DAC, the owner WRITE_OWNER, the SACL ACCESS_SYSTEM_SECURITY. Here txn_fd gives atomicity, not layer qualification (SDs are not layered), or -1 to apply immediately. SD changes affect only future opens — handles already open keep their fixed grant.

7.7 Backup and restore

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

int peios_reg_backup(int key_fd, int output_fd);
int peios_reg_restore(int key_fd, int input_fd);
  • peios_reg_backup exports the key and its entire subtree to output_fd (needs SeBackupPrivilege). It takes a read-only snapshot and performs no per-key access check — the privilege is the gate. Errors: EPERM/EACCES, EBADF (output not writable), ENOENT, ENOTSUP, EBUSY.
  • peios_reg_restore replaces the key and its entire subtree from input_fd (needs SeRestorePrivilege), applied in one transaction. Errors: EPERM/EACCES, EBADF (input not readable), EINVAL (malformed stream), EEXIST (GUID collision), EOVERFLOW.

7.8 Transactions

Peios / Developing for Peios / SDK Reference / registry.h — The Registry

A transaction batches key creates and mutating value/key operations into an atomic unit.

int peios_reg_begin_transaction(void);
int peios_reg_commit(int txn_fd);
int peios_reg_txn_status(int txn_fd, uint32_t *state_out, int *terminal_errno_out);
  • peios_reg_begin_transaction starts one and returns a transaction fd (initially unbound; it binds to a source on first use), or -1/ENOMEM. Pass this fd as the txn_fd argument to the create and mutating calls to enlist them. Closing the fd without committing aborts the transaction — so a transaction is abort-by-default, which makes error paths safe.
  • peios_reg_commit atomically applies everything enlisted. On success the fd is terminal — close it. Errors tell you what to do: EINVAL (already committed / never bound), EBUSY (write-lock contention — the transaction stays active, retry the commit), EIO (source failure — stays active), ETIMEDOUT.
  • peios_reg_txn_status reads a transaction's state: state_out receives the REG_TXN_* state, and terminal_errno_out receives the errno that ended it (0 while active or after a clean commit). Both may be NULL.

The lifecycle: begin → enlist operations by passing txn_fd → commit (retry on EBUSY/EIO) → close, or just close to abort.

8.1 event.h — Events (KMES)

Peios / Developing for Peios / SDK Reference / event.h — Events

<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.

8.1.1 See also #

  • <peios/msgpack.h> — building and parsing the payloads events carry.
  • Auditing — the operator-side view of the event and audit stream.

8.2 Emitting events

Peios / Developing for Peios / SDK Reference / event.h — 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 type such as "org.example.backup.snapshot.created", named as PGSS §6.3 requires — 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:

errnoCause
EPERMNo SeAuditPrivilege.
EINVALZero-length type, or a malformed payload.
ENOSPCPayload exceeds the size caps.
EAGAINRate-limited.
EFAULTBad 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);                      /* {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);

static const char type[] = "org.example.backup.snapshot.created";
const void *buf; ssize_t n = peios_mp_writer_bytes(w, &buf);
if (n >= 0)
    peios_event_emit(type, sizeof type - 1, buf, (uint32_t)n);
peios_mp_writer_free(w);

8.2.0.1 The emission policy #

#define PEIOS_EVENT_TIER_ESSENTIAL 0u
#define PEIOS_EVENT_TIER_STANDARD  1u
#define PEIOS_EVENT_TIER_VERBOSE   2u
#define PEIOS_EVENT_TIER_DEBUG     3u

typedef struct peios_event_policy peios_event_policy;

peios_event_policy *peios_event_policy_open(void);
void peios_event_policy_close(peios_event_policy *policy);
int  peios_event_policy_enabled(peios_event_policy *policy, const char *event_type,
                                uint16_t event_type_len, uint32_t tier);

An emitter must not write an event whose type the emission policy has switched off (PGSS §6.9), and should find that out before it builds the payload. peios_event_policy_enabled answers it: 1 means build and emit, 0 means build nothing. tier is the tier the type declares in its fragment. An essential type is always 1, without reading anything.

peios_event_emit does not check the policy itself. By the time it is called the payload is built, and skipping that work is the point of asking.

The handle caches each type's answer and watches Machine\Generic\Events, so a change an administrator commits applies to your next decision. If that key does not exist yet, it watches for its creation; if nothing can be watched, it reads the policy again at least once a second. If the registry cannot be read at all, the answer is the tier's default: on for standard, off for verbose and debug.

peios_event_policy_open fails only with ENOMEM; it does not need the registry to be there. peios_event_policy_enabled returns -1 with EINVAL only for caller error: a NULL handle or type, a malformed type (empty, not UTF-8, an empty segment, or a \, / or NUL in a segment), or an unknown tier. Registry failures never make it fail.

Use a handle from one thread at a time, and don't use it across fork(): the child shares the parent's watch descriptor and would take the parent's notifications. A child opens its own. The Rust binding's peios::event::EventPolicy adds a lock so it can be shared between threads. libp-go has its own implementation, event.Policy.

8.2.0.2 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.

8.3 Consuming events

Peios / Developing for Peios / SDK Reference / event.h — 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.

8.3.0.1 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.

8.3.0.2 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.

8.3.0.3 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.

9.1 msgpack.h — MessagePack codec

Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding

<peios/msgpack.h> is a small, self-contained MessagePack codec. It exists because KMES event payloads are MessagePack: the kernel only structurally validates a payload on emit — it does not build or interpret it — so userspace owns the encode and decode. This codec is that path, and its validator's acceptance is deliberately matched to the kernel's emit-time check, so a payload this codec produces and validates is guaranteed to be accepted by peios_event_emit.

You can use it as a general MessagePack codec, but its reason for being is events.

It has three parts: a heap-backed writer, a stack-allocatable reader, and a validator.

9.1.1 See also #

  • <peios/event.h> — the KMES events these payloads travel in.
  • Library conventions — the sticky-error builder model the writer follows.

9.2 Conventions

Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding

A few rules hold across the codec:

  • Integers are written in their smallest MessagePack form automatically — you write an int64/uint64 and the encoder picks the compact encoding.
  • str values must be valid UTF-8. Use bin for arbitrary bytes. The reader enforces this on str reads too.
  • A valid payload is exactly one top-level value, and an empty buffer is not valid. (A map or array at the top counts as that one value.)
  • The writer is sticky-error, exactly like the <peios/security.h> builders: the write calls cannot fail individually; the first error latches and surfaces at peios_mp_writer_bytes / peios_mp_writer_error.

9.3 Writer

Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding

typedef struct peios_mp_writer peios_mp_writer;

peios_mp_writer *peios_mp_writer_new(void);
void             peios_mp_writer_free(peios_mp_writer *w);
void             peios_mp_writer_reset(peios_mp_writer *w);

Create a writer, append values, take the bytes, free it (or reset to reuse). All the append calls return void — errors latch.

9.3.0.1 Scalars #

void peios_mp_write_nil(peios_mp_writer *w);
void peios_mp_write_bool(peios_mp_writer *w, bool v);
void peios_mp_write_int(peios_mp_writer *w, int64_t v);
void peios_mp_write_uint(peios_mp_writer *w, uint64_t v);
void peios_mp_write_float(peios_mp_writer *w, double v);
void peios_mp_write_str(peios_mp_writer *w, const char *s, size_t len);  /* UTF-8 */
void peios_mp_write_bin(peios_mp_writer *w, const void *b, size_t len);

Use peios_mp_write_int for signed and peios_mp_write_uint for unsigned values; both are stored in the smallest form. peios_mp_write_str takes UTF-8 with an explicit length (no NUL needed); peios_mp_write_bin takes arbitrary bytes.

9.3.0.2 Containers #

void peios_mp_write_array(peios_mp_writer *w, uint32_t count);
void peios_mp_write_map(peios_mp_writer *w, uint32_t count);

Write the header, then exactly the promised number of values. A map of count needs 2 * count values — count key/value pairs — written key, value, key, value…. An under- or over-filled container is not caught at the write call; it surfaces at peios_mp_writer_bytes, when the whole structure is validated.

/* {"user": "alice", "ok": true} */
peios_mp_write_map(w, 2);
peios_mp_write_str(w, "user", 4);  peios_mp_write_str(w, "alice", 5);
peios_mp_write_str(w, "ok", 2);    peios_mp_write_bool(w, true);

9.3.0.3 Extensions and raw bytes #

void peios_mp_write_ext(peios_mp_writer *w, int8_t ext_type, const void *b, size_t len);
void peios_mp_write_raw(peios_mp_writer *w, const void *b, size_t len);
  • peios_mp_write_ext writes a MessagePack extension value with a signed type id.
  • peios_mp_write_raw appends pre-encoded MessagePack bytes verbatim — the escape hatch for splicing in a value you already have encoded. The result is still structurally validated as a whole at peios_mp_writer_bytes, so you can't smuggle malformed bytes through it.

9.3.0.4 Taking the bytes #

ssize_t peios_mp_writer_bytes(peios_mp_writer *w, const void **out);
int     peios_mp_writer_error(const peios_mp_writer *w);

peios_mp_writer_bytes confirms the buffer is exactly one well-formed top-level value, then borrows it: it writes a pointer to the encoded bytes through out (valid until the next mutating call on w) and returns the length. Pass out == NULL to validate and get the length without borrowing. It returns -1 with errno — EINVAL on a latched error or a malformed/under-filled structure, ENOMEM on a prior allocation failure. peios_mp_writer_error returns the latched errno directly, or 0.

Because this call validates, a successful peios_mp_writer_bytes is your guarantee the bytes are emit-ready.

9.4 Reader

Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding

The reader is a cursor over a borrowed buffer — stack-allocatable, no heap, no free. It decodes one value at a time, advancing the cursor.

struct peios_mp_reader { uint64_t _opaque[4]; };   /* opaque — do not inspect */

void   peios_mp_reader_init(struct peios_mp_reader *r, const void *buf, size_t len);
size_t peios_mp_reader_remaining(const struct peios_mp_reader *r);

Declare a struct peios_mp_reader locally and peios_mp_reader_init it over your buffer before use. buf may be NULL only when len is zero. Borrowed str/bin/ext pointers the reader hands back point into the original buffer and are valid for as long as it lives. peios_mp_reader_remaining reports the unconsumed byte count.

9.4.0.1 Peeking #

enum peios_mp_type {
    PEIOS_MP_NIL, PEIOS_MP_BOOL, PEIOS_MP_INT, PEIOS_MP_FLOAT,
    PEIOS_MP_STR, PEIOS_MP_BIN, PEIOS_MP_ARRAY, PEIOS_MP_MAP, PEIOS_MP_EXT,
};

int peios_mp_peek(const struct peios_mp_reader *r);

peios_mp_peek returns the peios_mp_type of the next value without consuming it, or -1 at end-of-input or on an invalid lead byte. Note that integers of every width and sign report as PEIOS_MP_INT — read them with peios_mp_read_int or peios_mp_read_uint as you prefer. Peek is how you drive a dispatch over a value whose type you don't know ahead of time.

9.4.0.2 Reading scalars #

int peios_mp_read_nil(struct peios_mp_reader *r);
int peios_mp_read_bool(struct peios_mp_reader *r, bool *out);
int peios_mp_read_int(struct peios_mp_reader *r, int64_t *out);
int peios_mp_read_uint(struct peios_mp_reader *r, uint64_t *out);
int peios_mp_read_float(struct peios_mp_reader *r, double *out);

Each consumes one value on success (returns 0) and leaves the cursor untouched on a type mismatch or truncation (-1 with errno == EINVAL) — so a failed read is safe to follow with a different-typed read or a peek. The out pointer is optional: pass NULL to consume/type-check a value without receiving its payload.

9.4.0.3 Reading strings, bytes, containers, extensions #

ssize_t peios_mp_read_str(struct peios_mp_reader *r, const char **out);
ssize_t peios_mp_read_bin(struct peios_mp_reader *r, const void **out);
ssize_t peios_mp_read_array(struct peios_mp_reader *r);
ssize_t peios_mp_read_map(struct peios_mp_reader *r);
ssize_t peios_mp_read_ext(struct peios_mp_reader *r, int8_t *type_out, const void **out);
int     peios_mp_skip(struct peios_mp_reader *r);
  • peios_mp_read_str / peios_mp_read_bin borrow the bytes (a pointer into the reader's buffer via out) and return the length, or -1. Strings are not NUL-terminated — use the length — and peios_mp_read_str rejects invalid UTF-8.
  • peios_mp_read_array returns the element count; peios_mp_read_map returns the key/value pair count (so read 2 * count values). After the header you read that many values yourself.
  • peios_mp_read_ext borrows an extension value's bytes, reporting its signed type id through type_out (both type_out and out are independently optional), and returns the data length.
  • peios_mp_skip consumes exactly one complete value, descending into nested containers — the way to ignore a value (or a whole subtree) you don't care about. 0 / -1.
struct peios_mp_reader r;
peios_mp_reader_init(&r, payload, payload_len);

ssize_t pairs = peios_mp_read_map(&r);          /* top-level map */
for (ssize_t i = 0; i < pairs; i++) {
    const char *key; ssize_t klen = peios_mp_read_str(&r, &key);
    /* dispatch on key… then read or skip the value */
    peios_mp_skip(&r);
}

9.5 Validator

Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding

int peios_mp_validate(const void *buf, size_t len, uint32_t max_depth);

peios_mp_validate confirms buf/len is exactly one well-formed MessagePack value: UTF-8 strings, nesting bounded by max_depth, no trailing bytes, non-empty. Returns 0 if valid, -1 with errno == EINVAL otherwise.

Crucially, its acceptance matches the kernel's emit-time check, so a 0 return means the event emit calls will accept the payload — at this depth bound. Pass KMES_CONFIG_MAX_NESTING_DEPTH_DEFAULT (32) for the default emit limit; the top-level value is depth 1. Validate before emitting when a payload comes from an untrusted or dynamic source, so you turn a would-be EINVAL from the kernel into a check you control.

10.1 rsi/source.h — Becoming a source

Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source

<rsi/source.h> is where a registry source begins. A source is a storage backend for the LCS registry — the provider counterpart to libpeios's registry client. Where a client opens keys and reads values, a source is what actually holds those keys and values and answers the kernel's requests for them.

This header has one job: registration. You declare which hives your process backs, register with the kernel, and get back a source fd. From that point on you serve the RSI (Registry Source Interface) protocol on that fd — reading requests and writing responses. Registration requires SeTcbPrivilege. The RSI wire constants (RSI_HIVE_PRIVATE, RSI_*) come from <pkm/lcs.h>.

This is part of librsi, a separate library from libpeios — link -lrsi and include <rsi.h> (or the individual <rsi/*.h>). It follows the same library conventions: raw fds, int returning 0/-1+errno, and the errno passed straight through from the kernel.

10.1.1 See also #

  • Registry sources overview — what a source is and how the RSI protocol flows.
  • The registry — the operator-side model of hives, layers, and sources.

10.2 Describing a hive

Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source

A hive is a subtree of the registry with its own root key. A source declares one struct rsi_hive per hive it backs:

struct rsi_hive {
    const void *name;         /* hive name (not NUL-terminated) */
    uint32_t    name_len;
    uint32_t    flags;        /* RSI_HIVE_PRIVATE, or 0 for a global hive */
    uint8_t     root_guid[16];/* root key GUID */
    uint8_t     scope_guid[16];/* private hives; zero for a global hive */
};
FieldMeaning
name / name_lenThe hive's name, length-counted (not NUL-terminated).
flagsRSI_HIVE_PRIVATE for a private (scoped) hive, or 0 for a global one.
root_guidThe GUID of the hive's root key — the anchor every path in the hive resolves from.
scope_guidFor a private hive, the scope GUID that bounds who can resolve it; zero for a global hive.

A global hive is visible system-wide; a private hive is scoped by scope_guid and resolvable only by tokens holding that scope (see the token LCS credentials). Set RSI_HIVE_PRIVATE and a non-zero scope_guid together for a private hive; leave both clear for a global one.

10.3 Registering

Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source

int rsi_register(const struct rsi_hive *hives, uint32_t count, uint64_t max_sequence);

Opens /dev/pkm_registry and registers all count hives in one call, returning the source fd — the descriptor you then read(2) requests and write(2) responses on — or -1 with errno.

ArgumentMeaning
hives / countThe hives this source serves. count must be >= 1; the kernel enforces its configured MaxHivesPerSource limit.
max_sequenceThe highest sequence number this source has already persisted. The kernel resumes its global sequence counter past this value, so a source that has durable state from a previous run must report it here to avoid reusing sequence numbers. A fresh source with no persisted state passes 0.

Errors include EPERM (no SeTcbPrivilege), EINVAL, ENOSPC (over the hive limit), ENOMEM, EFAULT, and any error from the underlying /dev/pkm_registry open(2).

struct rsi_hive hive = {
    .name = "MyStore", .name_len = 7,
    .flags = 0,                                  /* global hive */
    .root_guid = { /* … 16 bytes … */ },
};

int src = rsi_register(&hive, 1, /*max_sequence=*/0);
if (src < 0) { perror("rsi_register"); return -1; }
/* `src` is now the source fd — serve the RSI protocol on it. */

The max_sequence parameter is the one piece of state a durable source must get right: on restart, scan your persisted data for the highest sequence you ever wrote and pass it, so the kernel never hands out a sequence number you've already used.

10.4 What comes next

Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source

Registration is the whole of this header. Once you hold the source fd, the serve loop lives in the other two:

  • <rsi/request.h> — read and decode the requests the kernel sends.
  • <rsi/response.h> — build and send the replies.

The serving requests guide ties them together into a working serve loop.

11.1 rsi/request.h — Decoding requests

Peios / Developing for Peios / SDK Reference / rsi/request.h — Decoding Requests

<rsi/request.h> is the receiving half of a registry source's serve loop. The kernel sends your source RSI requests — "look up this child", "store this value", "begin this transaction" — as framed messages on the source fd. This header reads one frame, splits its header from its payload, and decodes the payload into a flat, typed struct you can act on.

The shape of the loop is always: read a frame → parse the header → dispatch on the op-code → decode the payload with the matching parser. The decoders are thin wrappers over the kernel's own RSI parsers, so your wire handling is guaranteed compatible with what the kernel sent.

Borrowing: every decoded name/data field is a (ptr, len) pair that borrows into your frame buffer. The pointers are valid only until you reuse that buffer for the next rsi_read_request. Copy out anything you need to keep across iterations. This is the same borrow discipline as libpeios's views.

Op-code and field constants (RSI_LOOKUP, RSI_WRITE_KEY_FIELD_*, RSI_TXN_*) come from <pkm/lcs.h>.

11.1.1 See also #

  • <rsi/response.h> — building the reply each op expects.
  • Serving requests — the read/parse/dispatch/respond loop in full.

11.2 Reading and parsing a frame

Peios / Developing for Peios / SDK Reference / rsi/request.h — Decoding Requests

struct rsi_request {
    uint64_t    request_id;   /* echo this in the response */
    uint64_t    txn_id;       /* transaction id (0 outside a transaction) */
    const void *payload;      /* borrowed; valid until the frame is reused */
    uint32_t    payload_len;
    uint16_t    op_code;      /* RSI_LOOKUP, RSI_SET_VALUE, … — dispatch on this */
};

ssize_t rsi_read_request(int fd, void *buf, size_t cap);
int     rsi_parse_request(const void *frame, size_t len, struct rsi_request *out);
  • rsi_read_request reads one framed request from the source fd into buf — a thin read(2) wrapper that blocks until a request is queued, then returns the frame length (pass it to rsi_parse_request). It returns 0 at EOF (the source is closing — leave the loop) or -1 with errno, notably EMSGSIZE if cap is smaller than the pending frame (size buf generously, or grow and retry).
  • rsi_parse_request splits a frame into its header and payload view, filling out with the request_id (which you must echo in the response), the txn_id (0 when the request is not inside a transaction), the op_code to dispatch on, and a borrowed payload pointer. Returns 0, or -1 with errno (EINVAL on NULL args, EBADMSG on a malformed frame).

11.3 The decoders

Peios / Developing for Peios / SDK Reference / rsi/request.h — Decoding Requests

Each decoder takes the parsed req and fills a flat struct: GUIDs by value, names and data as borrowed (ptr, len) pairs. All return 0, or -1 with errno — EINVAL if the arguments are NULL or the decoder doesn't match req->op_code (so calling the wrong decoder for an op is a clean error), and EBADMSG on a malformed payload. You dispatch on req.op_code and call the matching one.

11.3.0.1 Path and entry operations #

These operate on the name→GUID bindings that make up the key hierarchy. A child is named under a parent GUID, and entries live in layers.

/* LOOKUP — is child_name visible under parent_guid? */
struct rsi_lookup {
    uint8_t     parent_guid[16];
    const void *child_name;  uint32_t child_name_len;
};
int rsi_request_lookup(const struct rsi_request *req, struct rsi_lookup *out);

/* CREATE_ENTRY — bind child_name → child_guid in layer_name. */
struct rsi_create_entry {
    uint8_t     parent_guid[16];
    uint8_t     child_guid[16];
    const void *child_name;  uint32_t child_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint64_t    sequence;
};
int rsi_request_create_entry(const struct rsi_request *req, struct rsi_create_entry *out);

/* HIDE_ENTRY — tombstone child_name in layer_name. */
struct rsi_hide_entry {
    uint8_t     parent_guid[16];
    const void *child_name;  uint32_t child_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint64_t    sequence;
};
int rsi_request_hide_entry(const struct rsi_request *req, struct rsi_hide_entry *out);

/* DELETE_ENTRY — remove child_name's entry in layer_name. */
struct rsi_delete_entry {
    uint8_t     parent_guid[16];
    const void *child_name;  uint32_t child_name_len;
    const void *layer_name;  uint32_t layer_name_len;
};
int rsi_request_delete_entry(const struct rsi_request *req, struct rsi_delete_entry *out);

/* ENUM_CHILDREN — list the children of parent_guid. */
struct rsi_enum_children { uint8_t parent_guid[16]; };
int rsi_request_enum_children(const struct rsi_request *req, struct rsi_enum_children *out);
OpYou mustReply with
LOOKUPResolve child_name under parent_guid across your layers.rsi_respond_lookup
CREATE_ENTRYBind child_name → child_guid in layer_name at sequence.status
HIDE_ENTRYPlace a tombstone for child_name in layer_name.status
DELETE_ENTRYRemove child_name's entry in layer_name.status
ENUM_CHILDRENList every child of parent_guid.rsi_respond_enum_children

11.3.0.2 Key operations #

These operate on key metadata records — the non-layered facts about a key (its name, parent, security descriptor, flags).

/* CREATE_KEY — create the metadata record guid under parent_guid. */
struct rsi_create_key {
    uint8_t     guid[16];
    uint8_t     parent_guid[16];
    const void *name;  uint32_t name_len;
    const void *sd;    uint32_t sd_len;
    uint8_t     volatile_key;  /* 1 if volatile */
    uint8_t     symlink;       /* 1 if a symlink */
};
int rsi_request_create_key(const struct rsi_request *req, struct rsi_create_key *out);

/* READ_KEY / DROP_KEY — a request carrying just a key GUID. */
struct rsi_key_guid { uint8_t guid[16]; };
int rsi_request_read_key(const struct rsi_request *req, struct rsi_key_guid *out);
int rsi_request_drop_key(const struct rsi_request *req, struct rsi_key_guid *out);

/* WRITE_KEY — update the mutable fields of guid named by field_mask. */
struct rsi_write_key {
    uint8_t     guid[16];
    uint32_t    field_mask;        /* RSI_WRITE_KEY_FIELD_SD | …_LAST_WRITE_TIME */
    const void *sd;  uint32_t sd_len;/* NULL when the SD bit is clear */
    uint64_t    last_write_time;   /* valid only when the time bit is set */
};
int rsi_request_write_key(const struct rsi_request *req, struct rsi_write_key *out);
OpYou mustReply with
CREATE_KEYStore the metadata record for guid (its name, parent, sd, and the volatile_key/symlink flags).status
READ_KEYReturn the metadata of guid.rsi_respond_read_key
DROP_KEYDelete the metadata record for guid.status
WRITE_KEYUpdate only the fields selected in field_mask — the SD when RSI_WRITE_KEY_FIELD_SD is set, the last_write_time when its bit is set — leaving the rest untouched.status

WRITE_KEY's field_mask is the important detail: sd is NULL unless the SD bit is set, and last_write_time is meaningful only when the time bit is set, so consult the mask before reading either.

11.3.0.3 Value operations #

These operate on the typed values stored on a key, each written into a layer.

/* QUERY_VALUES — read value_name (or all values when query_all) of guid. */
struct rsi_query_values {
    uint8_t     guid[16];
    const void *value_name;  uint32_t value_name_len;
    uint8_t     query_all;   /* 1 = every value (then value_name is ignored) */
};
int rsi_request_query_values(const struct rsi_request *req, struct rsi_query_values *out);

/* SET_VALUE — store value_name in layer_name with the given type/data. */
struct rsi_set_value {
    uint8_t     guid[16];
    const void *value_name;  uint32_t value_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint32_t    value_type;
    const void *data;  uint32_t data_len;
    uint64_t    sequence;
    uint64_t    expected_sequence;  /* CAS guard (0 disables) */
};
int rsi_request_set_value(const struct rsi_request *req, struct rsi_set_value *out);

/* DELETE_VALUE_ENTRY — remove value_name's entry in layer_name. */
struct rsi_delete_value_entry {
    uint8_t     guid[16];
    const void *value_name;  uint32_t value_name_len;
    const void *layer_name;  uint32_t layer_name_len;
};
int rsi_request_delete_value_entry(const struct rsi_request *req,
                                   struct rsi_delete_value_entry *out);

/* SET_BLANKET_TOMBSTONE — set or clear a blanket tombstone on layer_name. */
struct rsi_set_blanket_tombstone {
    uint8_t     guid[16];
    const void *layer_name;  uint32_t layer_name_len;
    uint8_t     set;         /* 1 = set, 0 = clear */
    uint64_t    sequence;
};
int rsi_request_set_blanket_tombstone(const struct rsi_request *req,
                                      struct rsi_set_blanket_tombstone *out);
OpYou mustReply with
QUERY_VALUESReturn value_name — or every value when query_all is 1 (then value_name is ignored) — plus any blanket tombstones.rsi_respond_query_values
SET_VALUEStore value_name of value_type in layer_name. Honour expected_sequence as a compare-and-swap guard (0 disables it) — reject with a non-OK status if the current sequence differs.status
DELETE_VALUE_ENTRYRemove value_name's entry in layer_name.status
SET_BLANKET_TOMBSTONESet (set == 1) or clear a blanket tombstone on layer_name, masking all lower values at once.status

11.3.0.4 Transaction operations #

The kernel drives transaction boundaries; your source honours them so a group of writes commits or aborts atomically.

/* BEGIN_TRANSACTION — open transaction_id in mode. */
struct rsi_begin_transaction {
    uint64_t    transaction_id;
    uint32_t    mode;   /* RSI_TXN_READ_WRITE (0) or RSI_TXN_READ_ONLY (1) */
};
int rsi_request_begin_transaction(const struct rsi_request *req,
                                  struct rsi_begin_transaction *out);

/* COMMIT_TRANSACTION / ABORT_TRANSACTION — a request carrying just a transaction id. */
struct rsi_transaction { uint64_t transaction_id; };
int rsi_request_commit_transaction(const struct rsi_request *req, struct rsi_transaction *out);
int rsi_request_abort_transaction(const struct rsi_request *req, struct rsi_transaction *out);
OpYou mustReply with
BEGIN_TRANSACTIONOpen transaction_id in mode (RSI_TXN_READ_WRITE or RSI_TXN_READ_ONLY); buffer subsequent writes tagged with this id.status
COMMIT_TRANSACTIONAtomically apply everything buffered under transaction_id.status
ABORT_TRANSACTIONDiscard everything buffered under transaction_id.status

Requests that belong to a transaction carry its id in req.txn_id; a txn_id of 0 means the request is outside any transaction.

11.3.0.5 Layer operations #

/* DELETE_LAYER / FLUSH — a request carrying just a length-prefixed name. */
struct rsi_name { const void *name;  uint32_t name_len; };
int rsi_request_delete_layer(const struct rsi_request *req, struct rsi_name *out);
int rsi_request_flush(const struct rsi_request *req, struct rsi_name *out);
OpYou mustReply with
DELETE_LAYERRemove the entire named layer, reporting the GUIDs of any keys it orphaned.rsi_respond_delete_layer
FLUSHDurably persist pending writes for the named hive, replying only once persistence is confirmed.status

12.1 rsi/response.h — Building responses

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

<rsi/response.h> is the sending half of a source's serve loop. After you handle a request, you reply on the source fd with a framed response. This header builds those frames for you: you pass the result as flat arrays, and librsi validates and heap-encodes the wire frame — you never hand-pack a byte.

Every response echoes the request's id and its op-code (OR'd with the response bit) and carries an RSI_* status. Most operations are status-only; five carry a payload on success. Any operation can report a non-OK status with the status-only helper.

For the wire, a response is a 14-byte header (echoed request id, op-code | RSI_RESPONSE_BIT) plus a 4-byte RSI_* status, followed by an op-specific payload for payload-bearing successes; multi-byte integers are little-endian and names/data are length-prefixed. You don't assemble any of that — the helpers do. Status and target-type constants (RSI_OK, RSI_PATH_TARGET_GUID, …) come from <pkm/lcs.h>.

12.1.1 See also #

  • <rsi/request.h> — decoding the request each of these replies to.
  • Building responses — choosing and filling the right responder.

12.2 Status codes

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

Every response carries exactly one of these statuses. The kernel translates a non-OK status into the errno the registry client sees, so send the code that matches what actually happened:

CodeWhen to send it
RSI_OKThe operation succeeded. Status-only ops report it via rsi_respond_status; the five payload-bearing ops must use their own helper.
RSI_NOT_FOUNDThe requested key, entry, value, or layer does not exist in your store (client sees ENOENT).
RSI_ALREADY_EXISTSA create collided with something that already exists (client sees EEXIST).
RSI_STORAGE_ERRORYour backing store failed — I/O error, corruption, anything the client can't fix (client sees EIO).
RSI_NOT_EMPTYThe operation needs the key to have no children, and it has some (client sees ENOTEMPTY).
RSI_TOO_LARGEThe data exceeds what the source is willing or able to store (client sees ENOSPC).
RSI_TXN_BUSYA transaction can't proceed right now — e.g. write-lock contention; the operation may be retried (client sees EBUSY).
RSI_INVALIDThe request is well-formed RSI but violates the source's rules or refers to something malformed (client sees EINVAL).
RSI_CAS_FAILEDA sequence-guarded write's expected_sequence did not match the current entry — the compare-and-swap lost (client sees EAGAIN and retries).
RSI_TXN_NOT_SUPPORTEDReply to BEGIN_TRANSACTION from a source that does not implement transactions (client sees ENOTSUP).

12.3 The response contract

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

All rsi_respond_* helpers return 0, or -1 with errno. A set of rules applies to every helper, and violating one is an EINVAL caller-contract error:

  • (ptr, len) pairs: a pointer may be NULL only when its length/count is zero.
  • Boolean fields (volatile_key, symlink, target types) must be exactly 0 or 1.
  • Hidden path targets (RSI_PATH_TARGET_HIDDEN) must carry an all-zero target_guid.
  • LOOKUP/ENUM_CHILDREN metadata must exactly cover the GUID path targets referenced — no missing metadata, no duplicates, no unreferenced entries.
  • DELETE_LAYER orphan GUIDs must be nonzero and unique.

Beyond EINVAL, any helper can also fail with ENOMEM (during validation or frame allocation), EOVERFLOW (validation arithmetic or the assembled frame too large), EIO (a short write), or the raw write(2) errno. Per-helper EINVAL additions are noted below.

12.4 Sending a pre-built frame

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

ssize_t rsi_write_response(int fd, const void *frame, size_t len);

Writes one already-built response frame to the source fd — a thin write(2) wrapper returning the bytes written, or -1 with errno. Most callers never need this; the rsi_respond_* helpers build and send. It exists for callers assembling frames by other means.

12.5 Status-only responses

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

int rsi_respond_status(int fd, const struct rsi_request *req, uint32_t status);

The workhorse. Use it for:

  • status-only ops on success — pass status = RSI_OK; and
  • any op reporting a non-OK status — a LOOKUP that found nothing, a SET_VALUE that failed a compare-and-swap, a permission error: reply with the appropriate RSI_* status here, whatever the op.

It fails with EINVAL on a bad req, an unknown status, or RSI_OK given for a payload-bearing op (those must use their own helper on success), plus EIO / the write error.

The rule of thumb: on failure, always rsi_respond_status; on success, rsi_respond_status unless the op is one of the five below.

12.6 Payload-bearing responses

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

Five operations return data on success. Each takes the result as flat arrays and encodes the frame for you.

12.6.0.1 LOOKUP #

struct rsi_path_entry {
    const void *layer;  uint32_t layer_len;
    uint8_t     target_type;   /* RSI_PATH_TARGET_GUID (0) / RSI_PATH_TARGET_HIDDEN (1) */
    uint8_t     target_guid[16];
    uint64_t    sequence;
};
struct rsi_key_metadata {
    uint8_t     guid[16];
    const void *sd;  uint32_t sd_len;
    uint8_t     volatile_key;
    uint8_t     symlink;
    uint64_t    last_write_time;
};

int rsi_respond_lookup(int fd, const struct rsi_request *req,
                       const struct rsi_path_entry *entries, uint32_t entry_count,
                       const struct rsi_key_metadata *metadata, uint32_t metadata_count);

Answers a LOOKUP with the resolved path entries for the child — one per layer that has a view of it, each either a GUID target or a HIDDEN (tombstone) target — plus the metadata for every key the entries reference. A RSI_PATH_TARGET_HIDDEN entry must carry an all-zero target_guid; the metadata must exactly cover the GUID targets. EINVAL if req is not a LOOKUP, a nonzero count has a NULL array, or an entry has invalid target/boolean fields, missing/duplicate metadata, or unreferenced metadata.

12.6.0.2 ENUM_CHILDREN #

struct rsi_child_entry {
    const void            *child_name;  uint32_t child_name_len;
    const struct rsi_path_entry *entries;  uint32_t entry_count;
};

int rsi_respond_enum_children(int fd, const struct rsi_request *req,
                              const struct rsi_child_entry *children, uint32_t child_count,
                              const struct rsi_key_metadata *metadata, uint32_t metadata_count);

Answers an ENUM_CHILDREN with each child — its name and the path entries that resolve it — plus the metadata for every referenced key. The same target/boolean/metadata-coverage rules as LOOKUP apply. EINVAL on the same conditions, scoped to ENUM_CHILDREN.

12.6.0.3 READ_KEY #

int rsi_respond_read_key(int fd, const struct rsi_request *req, const void *name,
                         uint32_t name_len, const uint8_t *parent_guid, const void *sd,
                         uint32_t sd_len, uint8_t volatile_key, uint8_t symlink,
                         uint64_t last_write_time);

Answers a READ_KEY with the key's non-layered metadata: its name, parent_guid, security descriptor (sd), the volatile_key/symlink flags, and last_write_time. EINVAL if req is not a READ_KEY, parent_guid is NULL, or a boolean field is invalid.

12.6.0.4 QUERY_VALUES #

struct rsi_value_entry {
    const void *value_name;  uint32_t value_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint32_t    value_type;
    const void *data;  uint32_t data_len;
    uint64_t    sequence;
};
struct rsi_blanket_entry {
    const void *layer_name;  uint32_t layer_name_len;
    uint64_t    sequence;
};

int rsi_respond_query_values(int fd, const struct rsi_request *req,
                             const struct rsi_value_entry *entries, uint32_t entry_count,
                             const struct rsi_blanket_entry *blankets, uint32_t blanket_count);

Answers a QUERY_VALUES with the value entries — each value's name, the layer it lives in, its type, data, and sequence — plus the blankets (the blanket tombstones on this key, each a layer and sequence). The kernel resolves precedence across the layers you report. EINVAL if req is not a QUERY_VALUES or a nonzero count has a NULL array.

12.6.0.5 DELETE_LAYER #

int rsi_respond_delete_layer(int fd, const struct rsi_request *req,
                             const uint8_t *orphaned_guids, uint32_t orphaned_count);

Answers a DELETE_LAYER with the GUIDs of the keys the deleted layer orphaned — a flat orphaned_count * 16-byte array. The GUIDs must be nonzero and unique. EINVAL if req is not a DELETE_LAYER or a nonzero count has a NULL array, a nil GUID, or a duplicate.

12.7 The five at a glance

Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses

Success responseOpPayload
rsi_respond_lookupLOOKUPpath entries + referenced key metadata
rsi_respond_enum_childrenENUM_CHILDRENchildren (name + path entries) + metadata
rsi_respond_read_keyREAD_KEYone key's non-layered metadata
rsi_respond_query_valuesQUERY_VALUESvalue entries + blanket tombstones
rsi_respond_delete_layerDELETE_LAYERorphaned key GUIDs

Every other op — and every failure of these — is rsi_respond_status.

Peios Learn — documentation for the Peios project.

Built with Trail.