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

Inspecting security state

Single-page view · as markdown

Inspecting security state

Peios / Peios Security Fundamentals / Inspecting security state

When something goes wrong with access control, the answer is almost always in the live kernel state — what's on the relevant token, what's on the object's SD, what session the calling thread belongs to. Static configuration (the directory, the registry) tells you what should be the case; the live kernel state tells you what is the case. Inspection is how you read it.

Peios exposes inspection through a handful of surfaces: pseudo-files under /proc for per-process tokens, securityfs entries under /sys/kernel/security/kacs/ for the calling thread and the active sessions, and the KACS_IOC_QUERY ioctl on any token fd for the full menu of structured queries. This page maps the surfaces and the access rules they share; later pages in this topic cover each in depth.

What you can inspect #

Three classes of state are inspectable:

StateLives inPrimary inspection surface
TokensPer-thread (primary or effective), reference-counted in the kernel/proc/<pid>/token, /proc/<pid>/task/<tid>/token, /sys/kernel/security/kacs/self, KACS_IOC_QUERY on a token fd
Logon sessionsPer-authentication-event, referenced by every token via auth_id/sys/kernel/security/kacs/sessions (text listing); TokenStatistics query on a token (to get the session ID)
ProcessesPer-process, PSB plus process SD plus token referencesThe process's token fd (for token state); querying the PSB (for PIP, mitigations); reading the process SD via kacs_get_sd

The fourth thing one might expect — inspecting security descriptors on arbitrary objects (files, registry keys) — is covered by kacs_get_sd, the read counterpart of kacs_set_sd. That surface is part of the file-access and registry-access topics, not this one. This topic covers inspection of identity-and-process state specifically.

What you cannot inspect #

A few things that are not inspectable through this topic's surfaces:

  • Per-thread state for threads other than the inspection target. The token fd you get from /proc/<pid>/token is the primary token (a process-wide property). For thread-specific impersonation state, you need the thread-specific path /proc/<pid>/task/<tid>/token.
  • Internal kernel state. Reference counts, internal locks, cache state — these are not exposed. The inspection surfaces expose user-meaningful state only.
  • Historical state. A token that has been destroyed is gone; its fields cannot be recovered. The kernel does not retain a history of tokens. For historical analysis you need the audit log.
  • Other principals' state without authority. Reading another process's token requires PROCESS_QUERY_INFORMATION on the process plus PIP dominance plus the token access rights. The surfaces enforce all three. There is no "global view" available to a non-privileged caller.

Who can inspect what #

The access rules for inspection mirror the access rules for the underlying state. The kernel does not have a separate "inspection" privilege; reading state requires the same rights that any other read of that state would require.

For your own state (your thread's effective token, your process's primary token, the sessions you are part of):

  • /proc/self/token, /proc/self/task/<self-tid>/token — always readable.
  • /sys/kernel/security/kacs/self — always readable.
  • Token fd from kacs_open_self_token — always returnable to you.

For another process's state:

  • /proc/<pid>/token requires PROCESS_QUERY_INFORMATION on the target process plus PIP dominance.
  • /proc/<pid>/task/<tid>/token requires the same.
  • Token fd from kacs_open_process_token requires the same plus the appropriate token rights.

For session state:

  • /sys/kernel/security/kacs/sessions requires BUILTIN\Administrators or SYSTEM (enforced by the SD on the securityfs file).
  • Individual session details accessible via tokens you can already read (your own, or others' subject to the above rules).

For process state beyond the token (process SD, PIP, mitigations):

  • Read your own PSB via kacs_open_self_token or querying through the process-token's interface — always.
  • Read another process's PSB requires PROCESS_QUERY_INFORMATION plus PIP dominance.

The pattern: self is free, others need standard cross-process authority. PIP dominance is the absolute ceiling that nothing — no privilege, no inspection surface — bypasses. The kernel does not expose state of higher-trust processes to lower-trust callers under any conditions.

Two ways to read a token #

The kernel exposes two complementary ways to read what a token holds:

  • Get a token fd, then query via KACS_IOC_QUERY. This is the structured API. The ioctl takes a query class (one of 24 numbered classes) and returns binary data structured according to that class. Suitable for programmatic inspection.
  • Read a pseudo-file under /proc or /sys/kernel/security/kacs/. Some of these expose token fds (open them with O_PATH semantics and pass to ioctls); others expose text formats suitable for direct reading.

Both routes converge on the same kernel state. The pseudo-file route is convenient for shell-level inspection; the ioctl route is the right tool for programmatic use. A monitoring tool will typically use the ioctl; a sysadmin debugging an issue at a terminal will typically cat a pseudo-file.

The pseudo-files under /proc/<pid>/token are not text — they are token fds. cat-ing them does not produce human-readable output. The way to use them from the shell is the token command, which knows how to interpret a token fd and render its contents as readable text.

The /sys/kernel/security/kacs/sessions file is a text format — designed to be cat-able. It is the exception; the other surfaces are binary.

Standard query patterns #

A handful of patterns come up repeatedly:

  • "Who is this thread?" — Open /proc/<pid>/task/<tid>/token, query class TokenUser to get the user SID.
  • "What groups are on this token?" — Open the token, query the groups class.
  • "What privileges are enabled?" — Open the token, query the privileges class.
  • "Which session does this process belong to?" — Open the process's primary token, query TokenStatistics to get the auth_id, then look up that ID in /sys/kernel/security/kacs/sessions.
  • "Who owns this session?" — Either query TokenStatistics (returns the auth_id) and look up the session in the listing, or read the listing directly and find the session by its details.
  • "What PIP level is this process at?" — Read the PSB through the process's token-related surfaces.

The two-step lookup for sessions (token → auth_id → sessions listing) is the standard pattern. Tokens know which session they belong to via auth_id; the session listing has the human-readable details (logon type, auth package, user SID, creation time).

Where to start #

If you want to inspect a token — what fields it has, how to query each one, the two-call pattern for variable-length data — read Inspecting tokens.

If you want to inspect a session — the text listing format, how to find which tokens belong to a session, how to track session lifecycle — read Inspecting sessions.

If you want to inspect a process — the process SD, the PSB's PIP and mitigation fields, the rules for cross-process inspection — read Inspecting processes.

If you want to inspect the audit event stream rather than static state — the live flow of events as access checks fire — read The event stream. That page covers revstrm, a low-level diagnostic probe that taps the raw KMES stream directly. It is a debugging tool for the audit pipeline, not the everyday way to view events (that is eventd's job).

If you have a denial in front of you and want a systematic walk through the diagnosis, Debugging a denial is the right page.

Inspecting tokens

Peios / Peios Security Fundamentals / Inspecting security state

A token's fields are read through the KACS_IOC_QUERY ioctl on a token fd. The ioctl takes a query class — a small integer naming what to return — and returns the corresponding data structured per that class. There are 24 defined classes covering everything from "the user SID" to "the full set of user and device claims".

This page covers how to obtain a token fd, the query ioctl mechanics, the two-call pattern for queries with variable-length output, and an overview of the class catalog.

Obtaining a token fd #

Token fds come from a handful of syscalls and pseudo-files:

SourceReturnsAccess check
kacs_open_self_tokenThe calling thread's effective token (or primary, with KACS_REAL_TOKEN flag)None — always succeeds
kacs_open_process_token(pidfd)A target process's primary tokenPROCESS_QUERY_INFORMATION + PIP dominance + token SD rights
kacs_open_thread_token(tid)A specific thread's effective tokenSame as above
kacs_open_peer_token(sock_fd)The peer's captured identity on a connected Unix socketNone beyond the connection itself
/proc/<pid>/tokenThe primary token of process <pid>PROCESS_QUERY_INFORMATION + PIP dominance
/proc/<pid>/task/<tid>/tokenThe effective token of thread <tid> in process <pid>Same
/sys/kernel/security/kacs/selfThe calling thread's effective tokenNone — always readable

The fds carry an access mask. The mask is what the kernel granted at open time and what the subsequent ioctl will check against. A fd opened with TOKEN_QUERY cannot be used to install or duplicate the token; the ioctl will see the request as exceeding the fd's mask and refuse.

The pseudo-files under /proc and /sys/kernel/security/kacs/ return read-only fds — they carry TOKEN_QUERY and nothing else. To get a fd with more access you need one of the syscalls.

KACS_IOC_QUERY #

The ioctl is straightforward in shape:

ioctl(token_fd, KACS_IOC_QUERY, &args)

Where args is a kacs_query_args struct:

FieldMeaning
token_classThe numeric class identifying what to return (1–24 in v0.20).
buf_lenInput: the size of the output buffer in bytes. Output: the actual number of bytes the query needed.
buf_ptrUserspace pointer to the output buffer.

The kernel:

  1. Validates the class against the catalog. Unknown classes return -EINVAL.
  2. Checks that the fd grants TOKEN_QUERY. If not, returns -EACCES.
  3. Computes the size the response needs.
  4. If buf_ptr is zero or buf_len is zero — this is a size query — writes the required size to buf_len and returns 0.
  5. If buf_ptr is non-zero but buf_len is smaller than required, returns -ERANGE with the required size still written to buf_len.
  6. Otherwise writes the response to the buffer and returns 0.

The "two-call pattern" — size query then fetch — is the standard way to handle variable-length output:

  1. Call once with buf_ptr = NULL (or buf_len = 0). The kernel writes the required size into buf_len and returns 0.
  2. Allocate a buffer of the indicated size.
  3. Call again with buf_ptr set to the buffer and buf_len set to its size. The kernel writes the response.

For classes with a fixed-size response, a single call with a buffer of the known size works in one go. The two-call pattern is needed only for classes whose response size depends on the token's contents (the groups class, the restricted-SIDs class, the default-DACL class, the claims classes).

The ioctl is idempotent — multiple queries for the same class produce the same result as long as the token has not been modified. Tokens carry a modified_id counter that increments on adjustment; if a query is part of a pipeline that depends on consistency across multiple queries, the modified_id can be queried first to detect mid-pipeline changes.

Query class catalog #

There are 24 defined query classes. Each returns a structured payload defined for that class. The most commonly used:

ClassReturns
TokenUserThe token's user_sid and its attributes.
TokenGroupsThe groups array — every group SID with its attributes. Variable length.
TokenPrivilegesThe four privilege bitmasks — present, enabled, enabled-by-default, used.
TokenOwnerThe default owner SID.
TokenPrimaryGroupThe default primary group SID.
TokenDefaultDaclThe token's default DACL. Variable length.
TokenSourceThe source name and source-LUID identifying who minted the token.
TokenTypePrimary or Impersonation.
TokenImpersonationLevelAnonymous / Identification / Impersonation / Delegation (Primary tokens return Anonymous).
TokenStatisticstoken_id, auth_id (the logon-session ID), modified_id, token type, and expiry.
TokenRestrictedSidsThe restricted_sids array. Variable length.
TokenSessionIdThe interactive session ID.
TokenOriginThe originating logon-session ID.
TokenElevationTypeDefault / Full / Limited.
TokenIntegrityLevelThe integrity SID.
TokenMandatoryPolicyThe mandatory_policy flags (NO_WRITE_UP, NEW_PROCESS_MIN).
TokenLogonTypeHow the token's logon session was created — interactive, network, batch, service, and so on.
TokenLogonSidThe logon session's logon SID (S-1-5-5-X-Y).
TokenAppContainerSidThe confinement SID. Empty if the token is not confined.
TokenCapabilitiesThe confinement capability SIDs with their attributes. Variable length.

The remaining classes are TokenDeviceGroups (the device group SIDs), TokenUserClaims and TokenDeviceClaims (the claim arrays evaluated by conditional ACEs), and TokenProjectedSupplementaryGids (the token's projected Linux supplementary GIDs). Note there is no query class for the partner of a linked token pair — that goes through a separate ioctl, KACS_IOC_GET_LINKED_TOKEN, with its own access rules.

Each class's exact byte-level payload format is in the Wire formats reference; this page covers what each class is for.

Patterns by use case #

A handful of patterns come up repeatedly:

"Who is this thread acting as?" Open the thread's effective token (/proc/<pid>/task/<tid>/token or kacs_open_self_token). Query TokenUser to get the principal SID. Optionally query TokenImpersonationLevel to see if this is an impersonation token, and what level.

"What rights does this token have on this object?" This is not a query — you call AccessCheck with the token, the object's SD, and the access mask you want to test. Querying the token alone does not tell you the answer; the rights depend on the SD too.

"Which session does this token belong to?" Query TokenStatistics to get auth_id. Look up that ID in /sys/kernel/security/kacs/sessions for the session's details.

"Is this token elevated?" Query TokenElevationType. If Full, this token is the elevated half of a linked pair. If Default, it is not part of a pair. If Limited, it is the non-elevated half — the elevated counterpart is reachable via KACS_IOC_GET_LINKED_TOKEN.

"What privileges can this token actually exercise?" Query TokenPrivileges and inspect both the present and enabled bitmasks. A privilege is exercisable if it is both present and enabled. A privilege that is present but disabled can be enabled via AdjustPrivileges; a privilege that is absent cannot.

"Has this token been adjusted since I last looked?" Query TokenStatistics. The modified_id field is a counter that increments on every adjustment. If it has changed since your last query, the token has been adjusted.

What query classes do not let you do #

A few clarifications:

  • You cannot modify a token through a query class. Queries are read-only. Modification goes through AdjustPrivileges, AdjustGroups, AdjustDefault, or kacs_set_sd.
  • You cannot enumerate every token on the system. There is no "list all tokens" call. You can walk /proc/*/token to find tokens belonging to currently-running processes, but tokens held only by file descriptors with no associated running process are not enumerable.
  • You cannot read tokens you do not have authority for. A token fd with only TOKEN_QUERY lets you query, but the fd had to be opened with appropriate authority. The query ioctl does not bypass the access checks at open time.
  • You cannot query undefined classes. Class numbers outside the defined range (1–24) return -EINVAL. There are no hidden or reserved slots — all 24 defined classes are valid.

Reading from the shell #

For a sysadmin debugging at a terminal, the token command is the utility that wraps this ioctl. It handles the two-call pattern, decodes the binary payloads, and renders the results as readable text — so the query classes above become token subcommands rather than raw ioctl calls.

For programmatic use, the ioctl is what you call directly. Language bindings (the C SDK, the Python wrapper) provide ergonomic wrappers but ultimately call the same ioctl.

The pseudo-file approach — /proc/<pid>/token, /sys/kernel/security/kacs/self — gives you the token fd; the actual query still goes through the ioctl. Pseudo-files are just a convenient way to acquire the fd from the shell.

See also #

  • Inspecting security state — the topic overview and the shared access rules.
  • The token command — the shell wrapper around this ioctl.
  • Tokens — what the queried fields mean.
  • Wire formats reference — byte-level payload formats for each query class.

Inspecting sessions

Peios / Peios Security Fundamentals / Inspecting security state

Logon sessions are the kernel's records of authentication events. Every token belongs to one session; every running thread is acting under a token; therefore every running thread is associated with a session. Inspecting the system's active sessions tells you who is currently signed in, in what mode, when, and which authentication mechanism brought them in.

The primary inspection surface is /sys/kernel/security/kacs/sessions — a text-format pseudo-file that lists every active session. This page covers the listing format, the standard pattern for finding a session from a token, and how to track session lifecycle through the audit stream.

The sessions pseudo-file #

/sys/kernel/security/kacs/sessions is a text file produced by the kernel on each read. Each line describes one active session:

session_id=<decimal-u64> user_sid=<lowercase-hex-sid> logon_type=<decimal-u32> auth_package=<lowercase-hex-utf8> created_at=<decimal-u64>

Fields are space-separated, in key=value form. The format is stable for the listed fields; consumers should ignore unknown additional fields, which future versions may append.

FieldTypeMeaning
session_iddecimal u64The session's LUID. Same as the auth_id recorded on every token belonging to this session.
user_sidlowercase hexThe SID of the principal who signed in.
logon_typedecimal u32The logon type. See Logon types.
auth_packagelowercase hex of UTF-8The auth-package name (e.g. "Kerberos", "NTLM", "local") encoded as lowercase hex of the UTF-8 bytes.
created_atdecimal u64Creation timestamp (kernel-internal monotonic units).

The user_sid and auth_package fields are hex-encoded for parser stability — the SID is a binary structure, and the auth-package name could in principle contain characters that complicate text parsing. Hex encoding is uniform.

Access rule #

The file's SD grants read to BUILTIN\Administrators and SYSTEM only. A non-administrative caller will get EACCES on open(). This is intentional: the listing reveals every active session on the machine, including their identities and timestamps, which is information you do not want a low-privileged process to read.

For a sysadmin running as root (which projects to a token in the administrative group), reading the file is straightforward. For service accounts that need session enumeration capability, the right approach is to grant the relevant SID access via the file's SD, not to weaken the default protection.

Bootstrap sessions #

Two sessions exist before authd is up:

Session IDUse
0The SYSTEM session. Attached to init, inherited by every early-boot process. Stays present for the lifetime of the system.
998The Anonymous session. Backs the singleton Anonymous token.

Both appear in the listing. They are not bugs; their presence is the normal state of any running system.

Finding a session from a token #

The standard pattern: given a thread or process, find its session.

  1. Open the token. For a thread, use /proc/<pid>/task/<tid>/token. For a process's primary, use /proc/<pid>/token. For yourself, use kacs_open_self_token or /sys/kernel/security/kacs/self.
  2. Query TokenStatistics via KACS_IOC_QUERY. The response includes auth_id (the session's LUID).
  3. Look up auth_id in /sys/kernel/security/kacs/sessions to find the matching session_id. The line gives you the session's full details.

This is the standard "which session is this process in" query. The session ID is the key; the listing has the rest.

For programmatic enumeration ("for each running process, which session is it in"):

for each pid in /proc/*:
  for each tid in /proc/<pid>/task/*:
    open /proc/<pid>/task/<tid>/token
    query TokenStatistics, get auth_id
    cross-reference with the sessions listing

This produces a complete picture of which thread is in which session. The logonse command uses exactly this pattern — it walks the running processes to render which processes belong to which session.

Tracking session lifecycle #

Sessions are created and destroyed dynamically. The listing always reflects the current state; to track changes over time you need to either poll the listing or subscribe to session lifecycle events.

Session creation #

The kernel does not emit a kernel-level event when a session is created — there is no logon-session-created event in v0.20 audit. Tracking creations requires either:

  • Periodic polling of /sys/kernel/security/kacs/sessions and comparing against the previous snapshot.
  • Hooking into authd, which is the only thing that creates sessions and could in principle emit a higher-level event. (This is an authd integration concern, not a kernel one.)
  • Listening for the audit events that fire on successful authentications — these are emitted by authd and consumed via KMES.

For most monitoring purposes, the audit stream from authd is the right source. The kernel-level sessions listing tells you "what is right now"; the audit stream tells you "what happened recently".

Session destruction #

The kernel does emit a logon-session-destroyed event when a session loses its last token reference. The event includes the session ID, the user SID, the logon type, the auth-package name, and the creation timestamp — enough to reconstruct what the session was.

The event is documented in Events and transport. Tools that want to track session lifecycle subscribe to this event via KMES and write a record on each occurrence.

The pattern: at session creation (detected via authd or via polling), record the start; at the logon-session-destroyed event, record the end. The two together give you a complete session log.

Inspecting an individual session #

A session ID is just a u64, but the listing line gives you everything currently knowable about the session from the kernel's perspective. There is no separate "session detail" query that returns more than what the listing provides.

If you need detail beyond what the listing offers (the privileges granted at sign-in, the policy that applied, the auth-package's specific authentication flow), the source is authd's own state, accessible via authd's APIs. The kernel records the session existence and minimal metadata; authd records the rest.

This separation is the standard kernel/userspace split. The kernel knows the session exists, who it is for, when it was created, and what auth-package created it. authd knows what happened during authentication.

Counting tokens per session #

A session can have many tokens. The number of tokens belonging to a session is the count of:

  • Primary tokens attached to processes whose auth_id matches.
  • Impersonation tokens currently installed on threads whose auth_id matches.
  • Token fds open against tokens with that auth_id.

Counting these from the outside is awkward — there is no "tokens per session" query. The standard way is to walk /proc/*/token and /proc/*/task/*/token, query TokenStatistics on each, and count matches. This is what session-revocation tooling does (authd specifically).

The reason for the awkward enumeration: each token is a separate kernel object, and there is no per-session index. The kernel knows tokens reference sessions (via auth_id); it does not maintain a reverse index of which tokens reference which session. Walking the running processes is the way to find tokens that exist.

A token held only by a file descriptor with no associated running process — e.g., a token fd passed via SCM_RIGHTS to a recipient that hasn't installed it — is not enumerable by walking /proc. Such tokens still keep the session alive (refcount), but their existence is not visible to a session-enumeration tool. They will reveal themselves only when the holding process tries to use them.

Session expiry #

A session's created_at is set at creation; there is no expires_at in the listing. Sessions do not expire on a kernel timer. A session lives as long as its tokens have references; it ends when the last token reference drops.

Warning

Token expiration (a field on each token) is set by authd but not enforced by the kernel in v0.20. A token can have an expiration time in the past and still be valid for AccessCheck.

The token's session continues to exist regardless of any token's expiration value.

If a deployment needs strict session timeouts, the enforcement is in userspace. authd can monitor created_at against a policy maximum and revoke sessions whose age exceeds the limit. Revocation is the userspace-coordinated process described in Session lifecycle: authd walks /proc/*/token, finds tokens with the target auth_id, kills the holding processes.

The lack of kernel-side timer enforcement is a deliberate simplification. Adding kernel timers for session expiry would push expiry policy into the kernel; keeping it in userspace lets administrators define their own rules.

Where to go next #

For the rest of a process's inspectable state — its PSB and its process SD — read Inspecting processes.

For what sessions are, how they are created, and how revocation actually works, read Session lifecycle.

Inspecting processes

Peios / Peios Security Fundamentals / Inspecting security state

A process's inspectable state spans three things: its token (its identity), its PSB (its PIP labels and mitigation flags), and its process SD (the policy on the process as an object). Each is read through its own surface but the access rules are similar — your own state is always readable; another process's state needs PROCESS_QUERY_INFORMATION plus PIP dominance.

This page covers the per-process inspection surfaces beyond the token. Tokens are covered in Inspecting tokens; this page is about the PSB and the process SD.

What the PSB holds #

The Process Security Block is the per-process kernel structure with:

FieldMeaning
pip_typeThe process's PIP type (None / Protected / Isolated).
pip_trustThe PIP trust level within the type.
Mitigation flagsThe bitfield of enabled mitigations (WXP, LSV, TLP, CFIF, CFIB, PIE, SML, NO_CHILD, etc.).
security_descriptorThe process SD, governing cross-process operations.

These are the inspectable fields. Internal fields (refcounts, lock state) are not exposed.

Inspecting your own process #

For a thread inspecting its own process's PSB, the path is:

  1. Open the process's primary token via kacs_open_self_token (with the KACS_REAL_TOKEN flag if you need the primary specifically, not the impersonation). The returned fd lets you query token state.
  2. Query through the process-related classes — TokenSessionId and related — which return PSB-adjacent state where available.
  3. Read the process SD via kacs_get_sd with a self-targeted query (using the appropriate flags for "this process").

For some PSB fields, dedicated query routes exist:

  • The PIP fields can be read by querying the calling process's PSB through a dedicated path. The typical surface is via the token's session/process classes, which carry the PIP fields as part of the per-token snapshot.
  • The mitigation bitfield is readable from the process itself; the typical pattern is to query the PSB directly via the appropriate ioctl.

The exact API for reading the PSB is in the Kernel ABI reference; the conceptual point for this page is that all PSB fields are introspectable by the process itself, with no privilege required.

Inspecting another process's PSB #

To inspect another process's PSB, you need:

  • PROCESS_QUERY_INFORMATION on the target's process SD.
  • PIP dominance over the target (the caller's PIP must dominate the target's, per the two-check rule).

Both requirements apply. A token-bearing principal granted PROCESS_QUERY_INFORMATION cannot inspect a higher-PIP process even with the SD grant — the PIP check is independent.

Once both checks pass, the same query mechanisms work: open the target's primary token (via kacs_open_process_token), query through KACS_IOC_QUERY, read the process SD via kacs_get_sd.

The PIP dominance requirement is the same one that gates every cross-process operation. A low-trust caller cannot see into a high-trust process even via inspection. A SeDebugPrivilege-holder can bypass the SD check (PROCESS_QUERY_INFORMATION becomes trivially granted) but does not bypass PIP — a privileged debugger still cannot inspect TCB processes.

In practice, only peinit and processes signed at the same PIP level as the target can inspect TCB processes. Ordinary administrators with SeDebugPrivilege are blocked at the PIP layer.

Reading the process SD #

A process's SD is read via kacs_get_sd with the appropriate process-targeted flags. The call:

kacs_get_sd(target_pidfd, security_information, buf, buf_len, flags)

Returns the SD components requested (per the security_information mask). Self-targeted queries are always allowed; cross-process queries require READ_CONTROL on the target's process SD plus PIP dominance.

READ_CONTROL is one of the standard rights every SD-bearing object exposes; it appears in the DACL like any other right. By default the owner of an object has it implicitly (see Ownership).

For inspecting the SACL specifically — to see audit ACEs, mandatory labels, PIP trust labels, scoped policy references — ACCESS_SYSTEM_SECURITY is the right needed, not READ_CONTROL. That right is gated by SeSecurityPrivilege. So:

  • DACL: needs READ_CONTROL (typically held by the owner).
  • SACL: needs ACCESS_SYSTEM_SECURITY (typically held only by administrators with SeSecurityPrivilege).
  • Owner / primary group SIDs: need READ_CONTROL.

A non-privileged caller can read a process's DACL (if granted) but not its SACL. For administrative inspection of the full SD including SACL, SeSecurityPrivilege is the lever.

Reading mitigation flags #

The mitigation bitfield on the PSB is read via a dedicated query path. For your own process the read is trivial. For another process the same PROCESS_QUERY_INFORMATION + PIP dominance rules apply.

The bitfield is the same one the kernel uses internally:

FlagBitMeaning
WXP0x001Write-XOR-Execute enabled
TLP0x002Trusted Library Paths enabled
LSV0x004Library Signature Verification enabled
CFI (legacy)0x008CFIF + CFIB combined alias
UI_ACCESS0x010Reserved
NO_CHILD0x020Forbid fork/clone-new-process
CFIF0x040Forward CFI
CFIB0x080Backward CFI
PIE0x100PIE-only exec
SML0x200Speculation mitigation lock

A process's mitigation flags tell you what hardening it has enabled. Comparing this against the process's binary lets you reason about which exploitation paths are closed — a TCB-signed binary running with WXP, LSV, TLP, CFIF, CFIB, and PIE is comprehensively hardened; one with only PIE has minimal hardening.

The flags are one-way — once set, they cannot be cleared. So the snapshot you read now is also the snapshot for the rest of the process's life (except that new flags may be set). Re-reading produces the same or stricter result.

Cross-referencing process and token #

A common diagnostic pattern: given a process, know which session it is in, which user it acts as, what its PIP is, and what mitigations are active.

The sequence:

  1. Open the process's primary token via /proc/<pid>/token or kacs_open_process_token.
  2. Query TokenUser for the user SID.
  3. Query TokenStatistics for auth_id. Cross-reference with /sys/kernel/security/kacs/sessions for session details.
  4. Read the PSB for PIP and mitigations.
  5. Read the process SD for who can act on this process.

Each step requires the appropriate access, and each fails closed if the caller lacks authority over the target. For self-targeted queries everything succeeds.

For a debugger or monitoring tool, this is the standard "tell me everything about this process" workflow. The pieces are independent (each query is its own ioctl), but they combine to give a complete picture.

Live vs static state #

A process's state changes over time. The current state is what the inspection surfaces return; previous state is not retrievable.

The changing parts of a process's state:

  • Threads come and go. A process's set of threads is dynamic. Re-running per-thread inspection picks up the current set.
  • The thread's effective token may change (impersonation install/revert). Re-querying gets the current value.
  • The primary token's adjustable fields (privileges enabled state, groups enabled state, default DACL) can change. The modified_id counter on the token tracks how many changes have happened.
  • The process SD can be modified by anyone with WRITE_DAC on the process. New ACEs appear; old ACEs disappear.

The immutable parts of a process's state (once set):

  • PIP fields. Set at exec; never change for the lifetime of the process.
  • Mitigation flags (including NO_CHILD). One-way; can be tightened but never relaxed.
  • Token identity fields (user_sid, groups[].sid, restricted_sids, logon_sid). Set at token creation; the token can be replaced but never have its identity adjusted.

Knowing which fields are immutable helps with monitoring. A monitor that has already read the PIP fields once does not need to re-read them; they will not change. A monitor watching for privilege state changes needs to poll or subscribe to events — they can change at any time.

What inspection cannot tell you #

A few clarifications:

  • It cannot tell you what access a process has. Inspection gives you the inputs to AccessCheck (the token, the object's SD); it does not compute the access. To know what a process can do to a specific object, call AccessCheck.
  • It cannot give you a tamper-evident snapshot. The kernel may modify state between two reads; there is no "atomic snapshot" surface. Tools that need consistency should use the modified_id counter to detect changes.
  • It cannot reveal the contents of the target process's memory. Inspecting the PSB and the process SD shows you the kernel's metadata about the process. To read the process's memory you need PROCESS_VM_READ on the process SD plus PIP dominance plus a ptrace-like syscall. That is a different topic.
  • It cannot show you removed history. A token whose privileges were once enabled but have since been removed shows the current state, not the history. The used bit on a privilege is a sticky record of "this privilege has been exercised at some point", but specific timestamps are an audit-log concern, not an inspection concern.

The inspection surfaces are for the present moment. For historical questions, the audit log is the right source.

Where to go next #

For inspecting the live flow of audit events rather than current state, read The event stream.

For the identity half of a process's state — obtaining and querying token fds — read Inspecting tokens.

For the dominance rule that gates every cross-process inspection, read The two-check rule.

The event stream

Peios / Peios Security Fundamentals / Inspecting security state

The other pages in this topic inspect state — what a token holds, which session a thread belongs to, what a process is authorised for. This page inspects flow: the live stream of audit events the kernel produces as access checks fire. State tells you what is true right now; the event stream tells you what is happening, event by event, as it happens.

The tool for reading that stream raw is revstrm. It attaches directly to the KMES per-CPU ring buffers and prints every event it drains. It is deliberately oblivious to the rest of the audit pipeline: it knows nothing about eventd, applies no persistence, and makes no attempt at reliable delivery. It is a debugging probe for the transport layer itself.

What revstrm is (and is not) #

revstrm is not the normal way to look at audit events. The normal way is to query eventd, the userspace audit daemon that subscribes to KMES, persists events, and serves them to consumers. eventd is the durable, deployment-facing surface. revstrm sits underneath it, tapping the same kernel stream directly.

Two things make it worth having:

  • It is a diagnostic. When events are not reaching eventd, or you suspect the kernel is not emitting what you expect, revstrm lets you see the raw KMES stream with no daemon in the path. It is the "is the wire live?" probe for the audit pipeline.
  • It is the reference consumer. revstrm exercises the full KMES consumption protocol of PSPK §2.4 — one drain thread per CPU ring, the futex notification wait, generation-change re-attach, and lapping/gap detection. It is the worked example that proves the consumption path before a production consumer like eventd relies on it.

Because it taps KMES directly rather than through eventd, revstrm is subject to the raw transport's limits: it is its own KMES subscriber, it can fall behind, and when it does its ring laps and it loses events — visibly (see Lapping and gaps). It is not a lossless audit sink and must never be relied on as one. For durable audit, that is eventd's job.

Access requirement #

Attaching to the KMES ring buffers requires SeSecurityPrivilege. Without it, the attach fails at the first ring with a clear hint rather than a silent empty stream:

revstrm: cannot attach to the KMES ring buffers (SeSecurityPrivilege required)

This is the same privilege class that gates the rest of the security-sensitive surfaces: reading the audit stream reveals every access decision on the machine, so it is not something a low-privileged process can do. An administrator (whose token holds SeSecurityPrivilege) can run it; an ordinary user cannot.

Synopsis #

revstrm [OPTION]...

With no options, revstrm follows the live stream: it attaches to every per-CPU ring and prints each event as it arrives, oldest surviving event first, until you interrupt it (Ctrl-C) or the output pipe closes. There is no target to name and no subscription to configure — it drains whatever the kernel is currently writing to KMES.

Options #

The option surface is small and entirely about what to show and how to show it — there is nothing to configure about the subscription itself.

OptionDescription
-t, --type GLOBOnly show events whose event-type string matches GLOB. Repeatable; an event is shown if it matches any supplied pattern (OR). Uses shell-glob syntax (e.g. --type 'access-*').
-o, --origin CLASSOnly show events from origin CLASS, one of userspace, kmes, kacs, or lcs (case-insensitive; user/usr alias userspace). Repeatable; an event is shown if its origin matches any supplied class.
-p, --prettyExpand the msgpack payload across multiple indented lines instead of the compact one-line form.
-s, --snapshotDrain the events currently buffered across all rings and exit, instead of following the live stream. Single-threaded, in CPU order; there is nothing to wait for.
--helpPrint usage and exit.
--versionPrint version and exit.

Long options may be abbreviated as long as the prefix is unambiguous (e.g. --sn for --snapshot).

The --type and --origin filters are applied by revstrm after draining, purely to reduce what is printed — they are display filters, not a kernel-side subscription. The kernel still writes every event into the ring, and revstrm still drains every event; filtered-out events are simply not printed. This matters for lapping: filtering does not reduce the drain load, so it does not make revstrm less likely to fall behind.

Output format #

Each event prints as a single header line (followed, under --pretty, by an indented payload block):

TIME  cpuN  #SEQUENCE  ORIGIN  event.type  payload
FieldMeaning
TIMEUTC time-of-day with microsecond precision, HH:MM:SS.uuuuuu. The calendar date is dropped — a live tail cares about wall-clock time of day, not the day.
cpuNThe per-CPU ring the event was drained from. Events are sharded per CPU all the way down; revstrm prints the CPU rather than merging into a single ordered stream.
#SEQUENCEThe event's per-ring sequence number. Gaps in the sequence on a given CPU indicate lost events.
ORIGINThe origin class: one of USR, KMES, KACS, LCS (or cN for an unrecognised class).
event.typeThe event-type string (e.g. access-audit, logon-session-destroyed).
payloadThe msgpack payload, rendered as described below.

Payload rendering #

By default the payload is rendered compactly on the header line, truncated if long. revstrm decodes the msgpack and applies a couple of field-name conventions to make raw events readable:

  • Keys ending in sid are rendered as canonical string SIDs (S-1-5-18); keys ending in sids render as an array of them.
  • Keys ending in access are decoded into |-joined access-right names (e.g. FILE_READ_DATA|FILE_READ_ATTRIBUTES), with any unrecognised bits shown as a hex remainder so nothing is hidden.

A payload that will not decode as msgpack falls back to a hex preview rather than being dropped. With --pretty, the same payload is expanded into an aligned, indented block — one key per line, nested maps and arrays expanded beneath their key. Use --pretty when you are reading individual events closely; leave it off when tailing a busy stream.

The event schemas themselves — access-audit, continuous-audit, privilege-use, logon-session-destroyed — are documented in Events and transport. revstrm does not interpret them beyond the field-name conventions above; it is a stream printer, not an event analyser.

Example #

A short follow session might look like:

14:22:07.481923  cpu0  #10432    KACS  access-audit  {subject: {user_sid: S-1-5-21-…, integrity_level: 12288, …}, requested_access: FILE_READ_DATA|FILE_READ_ATTRIBUTES, granted_access: FILE_READ_DATA|FILE_READ_ATTRIBUTES, success: true, …}
14:22:07.492010  cpu3  #8871     KACS  privilege-use  {privilege: "SeBackupPrivilege", surviving_access: FILE_READ_DATA, success: true, …}
14:22:08.003114  cpu0  #10433    KACS  logon-session-destroyed  {session_id: 4051, user_sid: S-1-5-21-…, logon_type: 2, …}

Lapping and gaps #

revstrm is one KMES subscriber among possibly several, with its own per-CPU rings. If it cannot drain a ring fast enough — a burst of audit events, a slow terminal, a --pretty render on a busy stream — that ring laps: the kernel overwrites the oldest un-drained events with new ones. This is the flow-control behaviour of KMES, not a bug in revstrm.

When a ring laps, revstrm does not hide it. It prints a visible marker naming the CPU and the count lost:

--- cpu2: lost 37 event(s) (ring lapped) ---

A dropped event a debugger cannot see is the worst possible outcome, so lapping is always surfaced. If you see these markers, revstrm is not keeping up — narrowing the output with --type/--origin won't help (the drain still happens), but redirecting to a file, dropping --pretty, or reducing the event rate will. Sustained lapping is also a signal in its own right: the kernel is producing events faster than a single un-buffered consumer can drain them.

Gaps are also visible directly in the #SEQUENCE column: a jump in the per-CPU sequence number is a run of events that this subscriber never saw.

Exit behaviour #

  • In follow mode (the default), revstrm runs until interrupted or until stdout closes. A downstream head (or any reader) closing the pipe shuts revstrm down cleanly — it treats the broken pipe as "nothing left to print to" and exits, rather than being killed by SIGPIPE. If every per-CPU ring hits a fatal error, all drain threads exit and the process ends.
  • In snapshot mode (--snapshot), it drains what is currently buffered across all rings and exits immediately.

When to use it #

Use revstrm when you are debugging the audit pipeline — "are events being emitted at all?", "is eventd's problem upstream or downstream of KMES?", "what exactly is the kernel putting on the wire?". Use eventd (or whatever consumes it in your deployment) for everything else: durable audit, historical queries, and any consumption that must not lose events.

See also #

  • Inspecting security state — the state-inspection counterparts: tokens, sessions, and processes.
  • Events and transport — the schemas of the events revstrm prints.
  • Auditing — where the events come from.

Peios Learn — documentation for the Peios project.

Built with Trail.