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

Access control

Single-page view · as markdown

Access control overview

Peios / Developing for Peios / Access control

KACS — the Kernel Access Control Subsystem — is the heart of Peios's security model, and it is the part of the SDK you are most likely to reach for. This section is a tour: it explains how the pieces fit together and points you at the right guide (and the right reference page) for each task. If you have not yet read the library conventions, read those first — everything here assumes the error and buffer rules they describe.

The four nouns #

Almost everything in KACS is built from four things:

NounWhat it isSDK home
SIDThe unique binary name of a principal — a user, group, machine, or well-known system actor.security.h
Security descriptor (SD)What protects an object: its owner, its group, and the ACLs that grant or deny access. Built from ACEs, each naming a SID and an access mask.security.h
TokenThe runtime object that carries an identity — a user SID, groups, privileges, an integrity level, claims. Every access decision is made against a token. A token is a file descriptor.token.h
Access checkThe act of deciding whether a token may perform a desired access on an object, given the object's SD.access.h

The relationship is simple to state: an access check asks whether a token (the subject) is granted some access to an object protected by a security descriptor, and both the token and the SD are expressed in terms of SIDs.

The shape of a decision #

When you need to make an authorisation decision in your own code, the pattern is almost always the same three steps:

  1. Get the subject's token. Usually the caller's — often via peios_token_open_peer on a socket, so you learn who connected — or your own effective token (token_fd = -1).
  2. Get the object's security descriptor. You either hold it already, read it off a file with peios_file_get_sd, or build one with a peios_sd_builder.
  3. Run the check. peios_access_check tells you whether the desired rights are granted, and exactly which subset was granted.

The checking access guide walks that end to end.

Advisory versus enforced #

There is one distinction worth internalising early. The SDK's access check is advisory: it computes what the answer would be. It does not enforce anything — enforcement of a real operation (opening a file, adjusting a token) happens inside the kernel against the subject's own process security block.

So you use peios_access_check when your code is the resource manager: you own some object — a record in your database, a slot in your service — that isn't a kernel object, and you want to make the grant/deny decision using Peios identities and the same rules the kernel would apply. For actual kernel objects (files, tokens, registry keys), you don't pre-check and then act; you just act, and the kernel enforces, returning EACCES if denied.

Identity is not always your identity #

A recurring theme in KACS is acting as someone other than yourself:

  • Impersonation lets a service temporarily adopt a caller's identity so its access checks run as them — the way a server does work "on behalf of" a client without running as root and hand-rolling permission logic. See working with tokens.
  • Restricted tokens let you drop power — derive a strictly less-privileged token to hand to less-trusted code.
  • Integrity levels and confinement bound what a token can touch regardless of its SIDs.

These are what make KACS more than POSIX uid/gid, and the token module is where you reach for them.

Where to go in this section #

  • Working with tokens — who am I, who is calling me, and how to act as someone else.
  • Checking access — building a security descriptor and making a decision end to end.
  • Securing files — the native open and reading/writing file security descriptors.
  • Hardening a process — turning on process mitigations.

For the exhaustive per-function detail behind any of these, the reference section documents every symbol.

Working with tokens

Peios / Developing for Peios / Access control

A token is the runtime carrier of an identity, and it is a file descriptor. This guide covers the everyday token tasks; token.h is the exhaustive reference for every call and field.

Who am I? #

To inspect your own identity, open your effective token and query it:

int tok = peios_token_open_self(0, KACS_TOKEN_QUERY);
if (tok < 0) { /* errno */ }

unsigned char sid[PEIOS_SID_MAX_BYTES];
ssize_t n = peios_token_user(tok, sid, sizeof sid);   /* the user SID */

uint32_t il;
peios_token_integrity(tok, &il);                      /* integrity level RID */

struct peios_privilege_set privs;
peios_token_privileges(tok, &privs);                  /* held/enabled privileges */

close(tok);

peios_token_open_self gives you the effective token — if your thread is impersonating, that's the impersonated identity. Pass KACS_TOKEN_OPEN_REAL in the flags to get your process's real primary token regardless. The access argument is the handle rights you want; KACS_TOKEN_QUERY is enough to read.

Group SIDs and other list-valued classes come back as buffers you parse with the security.h views: read CLASS_GROUPS with peios_token_query, then peios_sid_array_parse it.

Who is calling me? #

The most useful token trick in a service is learning the identity of whoever connected to your socket. When a client connects over a Unix stream or seqpacket socket, KACS captures their token at connect() time, and you open it from the accepted connection:

int conn = accept(listener, NULL, NULL);
int caller = peios_token_open_peer(conn);   /* QUERY | IMPERSONATE rights */
if (caller < 0) { /* errno */ }

/* Now query the caller's identity, or impersonate them (below). */

This is local authentication with no passwords and no handshake — the kernel vouches for who is on the other end. The handle comes with fixed QUERY | IMPERSONATE | DUPLICATE rights — enough to inspect the caller, act as them, and (for a process launched on their behalf) turn the identity into a primary token, but never to adjust it.

It works in the other direction too: right after connect(), peios_token_open_peer(sock) on the client's socket returns the identity of whoever is listening (at Identification level — enough to check, not to act as), so a client can verify it reached the genuine service and not something else that bound the path.

Acting as the caller #

Once you hold a caller's (impersonation) token, you can impersonate them: adopt their identity on your current thread so every subsequent access check runs as them, not as your service. This is how you do work on a client's behalf without running privileged and re-implementing their permissions.

if (peios_token_impersonate(caller) != 0) { /* errno */ }

/* ... do the work here: file opens, access checks, etc. all run as the caller ... */

peios_token_revert();   /* back to your own identity */
close(caller);

Always pair peios_token_impersonate with peios_token_revert, ideally in the cleanup path, so a failure partway through can't leave your thread wearing someone else's identity. peios_token_revert is a safe no-op if you weren't impersonating.

The full flow for a request handler is: accept → peios_token_open_peer → peios_token_impersonate → serve the request → peios_token_revert → close. When the handler runs start to finish on one thread and has no other use for the token, peios_token_impersonate_peer(conn) collapses the open, impersonate and close into one call.

A client decides how far its identity travels: peios_socket_set_impersonation_level(sock, KACS_IMLEVEL_IDENTIFICATION) lets the server learn who it is without being able to act as it. And a client that writes on behalf of many identities over one connection — a service impersonating its callers through a connection pool — turns on peios_socket_set_pass_token(sock, true), after which every send carries whoever is impersonating at that moment and the server's peios_token_open_peer tracks it request by request.

Handing an identity to a manager #

Impersonation acts as someone on your thread. The other shape is handing an identity to another process so it can act — the pattern behind the Peinit jobs socket (PSPU §7): a submitter such as backupd holds a token for the user whose data it is backing up, and wants Peinit to run the backup job as that user, not as backupd. Nothing is impersonated; the token itself is attached to the request, and the kernel vouches that the sender could have acted as it.

int jobs = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
connect(jobs, ...);                                   /* who may submit is who may connect */

const char *req = "{\"command\":\"submit\",\"image_path\":\"/usr/bin/backup\"}";
if (peios_socket_send_message(jobs, req, strlen(req), user_token, NULL, 0, 0) < 0) {
    /* EACCES: user_token lacks TOKEN_IMPERSONATE; EPERM: the attach would lower its level */
}

Three things make this safe, and none of them is the manager's to get wrong:

  • The kernel gates the attach like an impersonation. You need TOKEN_IMPERSONATE on the handle, and the token arrives at the sending socket's impersonation level — so a token the sender could only identify, never act as, cannot be smuggled through. Peinit refuses anything below Impersonation. For a job that must reach remote systems as the user, set the socket to Delegation first: peios_socket_set_impersonation_level(jobs, KACS_IMLEVEL_DELEGATION) — and the attach then only succeeds if the token itself is at Delegation, since the level only ratchets down.
  • The receiver owns what it is handed. peios_socket_recv_message gives the manager a token handle with QUERY | IMPERSONATE | DUPLICATE — enough to inspect the identity, check its level with peios_token_impersonation_level, and duplicate it into a primary token for the new process, but never to adjust it.
  • No token means the sender's own. A request that attaches nothing is run as the connecting process's primary token: the manager opens it through peios_socket_peer_pidfd and peios_token_open_process — the kernel's handle on the peer, never a PID the peer named — which is fork-like and needs no privilege on the submitter's part.

Descriptors travel the same way: pass a socket for the job to serve on, or a pipe to receive its output, as SCM_RIGHTS on the same message. The full protocol — what the manager does with them and how the job is managed afterwards — is PSPU §7; the calls are in the reference.

From Rust the same pattern is peios::socket::send_message / recv_message, with the received Token and OwnedFds owned by the ReceivedMessage, and peios::socket::peer_pidfd for the fallback:

use std::os::fd::AsFd;
use peios::socket;
use peios::token::{ImpersonationLevel, Token, TokenAccess};

socket::send_message(jobs.as_fd(), request, Some(user_token.as_fd()), &[], 0)?;

// The manager's side. Every descriptor that arrived is owned by `msg`.
let msg = socket::recv_message(conn.as_fd(), &mut buf, 8, 0)?;
if msg.truncated || msg.control_truncated {
    return Err(refused());
}
let identity = match msg.token {
    Some(token) if token.impersonation_level()? >= ImpersonationLevel::Impersonation => token,
    Some(_) => return Err(refused()),   // below Impersonation
    None => {
        let pidfd = socket::peer_pidfd(conn.as_fd())?;
        Token::open_process(pidfd.as_fd(), TokenAccess::QUERY | TokenAccess::DUPLICATE)?
    }
};

Dropping power #

To run less-trusted code with less authority than you hold, derive a restricted token and hand it over. peios_token_restrict can delete privileges, demote groups to deny-only, and add restricting SIDs:

struct peios_token_restrict spec = {
    .privs_to_delete = KACS_SE_DEBUG_PRIVILEGE | KACS_SE_IMPERSONATE_PRIVILEGE,
    .flags           = KACS_TOKEN_RESTRICT_WRITE_RESTRICTED,
};
int weak = peios_token_restrict(my_primary, &spec);

The result is a strictly less-powerful token. Combined with integrity levels and confinement (both set when minting a token), this is the basis of sandboxing on Peios.

Minting tokens #

Creating a token from scratch requires SeCreateTokenPrivilege and is the province of authentication authorities, not ordinary programs. When you do need it, the token-spec builder is the ergonomic path — typed setters for the user SID, groups, privileges, integrity, claims, and the rest, then peios_token_builder_create. Mind the index convention for owner/primary-group references, and don't add the logon SID yourself — the kernel injects it.

Next #

  • Checking access — use a token to make an authorisation decision.
  • token.h reference — every token call in full.
  • Impersonation — the operator-side model and its two gates.

Checking access

Peios / Developing for Peios / Access control

This guide walks a complete access decision: you have an object to protect, a caller to check, and you want KACS to tell you what they're allowed. The exhaustive detail lives in access.h and security.h; here we put them together.

When to use this #

Reach for peios_access_check when your program is the resource manager — you own something that isn't a kernel object (a document, an API route, a record) and you want to gate it with Peios identities and rules. For actual kernel objects, don't pre-check; just perform the operation and let the kernel enforce.

Remember the check is advisory: it computes the answer, you enforce it.

Step 1 — describe what protects the object #

An object is protected by a security descriptor. If you don't already have one, build it. Say the resource should be readable and writable by its owner and read-only for a "viewers" group:

/* An ACL: allow OWNER full, allow VIEWERS read. */
peios_acl_builder *acl = peios_acl_builder_new();
peios_acl_builder_allow(acl, owner_sid, owner_len, KACS_ACCESS_ALL, 0);
peios_acl_builder_allow(acl, viewers_sid, viewers_len, KACS_ACCESS_READ, 0);

size_t acl_len;
const void *acl_bytes = peios_acl_builder_bytes(acl, &acl_len);

/* Wrap it in a security descriptor with an owner. */
peios_sd_builder *sd = peios_sd_builder_new();
peios_sd_builder_owner(sd, owner_sid, owner_len);
peios_sd_builder_dacl(sd, acl_bytes, acl_len);

size_t sd_len;
const void *sd_bytes = peios_sd_builder_bytes(sd, &sd_len);

(You can also write the descriptor as SDDL text and parse it — often easier when the policy is fixed.)

Step 2 — identify the subject #

The subject is a token. Most often it's the caller you opened from a socket, or your own effective token. In the request you pass either a token fd or -1 for your own effective token.

Step 3 — run the check #

Fill in a request and call. desired is what you're testing; mapping folds any generic rights to the object class's specific bits (use the class's published mapping — here we'll treat the object like a file):

struct peios_access_request req = {
    .token_fd = caller_fd,               /* or -1 for my own 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);

Step 4 — interpret the result #

if (rc == 0) {
    /* Every desired right was granted. */
} else if (errno == EACCES) {
    /* Denied. `granted` still holds what WAS allowed — e.g. maybe READ
       succeeded but WRITE did not. Decide per-right from `granted`. */
    bool may_read  = granted & KACS_ACCESS_READ;
    bool may_write = granted & KACS_ACCESS_WRITE;
} else {
    /* A real error: bad token fd, malformed SD, etc. */
    perror("access_check");
}

The key idea: a denial is not an error to log and bail on — it's the expected "no". And because granted is filled even on denial, you can ask for a broad set of rights in one call and read back exactly which subset the subject has, rather than probing right by right. Clean up the builders (peios_acl_builder_free, peios_sd_builder_free) and the token fd when done.

Per-property checks #

If your object has properties or property-sets with their own object ACEs, evaluate the whole tree in one call with peios_access_check_list: supply an object-type tree and get one result per node, so you learn (for instance) that a caller may read most of an object but not one protected field — without a separate check per field.

Auditing a decision #

Pass a non-NULL peios_access_audit to learn what a SYSTEM_AUDIT ACE match would log, and whether a staged central access policy would decide differently — the signal you watch when rolling out a policy change.

If your daemon guards objects of its own, name the object in the request's audit context, so the kernel's audit record of the decision says what was checked:

struct peios_audit_field name = {
    .key = "name", .value_type = PEIOS_AUDIT_STR,
    .bytes = "jellyfin", .len = strlen("jellyfin"),
};
uint8_t ctx[128];
ssize_t n = peios_audit_context_encode("service", &name, 1, ctx, sizeof ctx);
req.audit_context = ctx;
req.audit_context_len = (size_t)n;   /* check n >= 0 first */

Do not write an event of your own recording the grant or the refusal. The kernel's record is the audit record, and a SACL on the object decides whether it is written (PGSS §6.7).

Next #

  • access.h reference — every field of the request and the audit outputs.
  • Access decisions — the operator-side account of how KACS reaches a verdict (and how to debug a surprising denial).

Securing files

Peios / Developing for Peios / Access control

Peios files carry real security descriptors, and the SDK opens them with a native KACS open rather than POSIX open(). This guide covers the two everyday tasks: opening a file with a specific access, and reading or changing a file's security. The full surface is in file.h.

To keep the fragments readable, most error checks are elided here — every call below returns -1 with errno on failure, and real code must check each one (see Library conventions).

The native open #

peios_file_open is shaped like NtCreateFile: you state the access you want, what to do about existence (the disposition), any create options, and — when creating — the security descriptor to stamp on the new file. It returns an ordinary Linux fd whose granted access is fixed for the fd's lifetime, which means you can safely hand it to another process by SCM_RIGHTS, dup, or across exec: the fd carries exactly the access it was opened with.

Important

At kernel 8e0e22de3a59cad506bbbf8873de456e16ad272d, OPEN_IF or OVERWRITE_IF with a creator SD fails with EINVAL for an existing path. Plain OPEN with a creator SD fails with EOPNOTSUPP, and OVERWRITE with one fails with EINVAL. The pinned source findings resolve these cases, not every open-interface discrepancy. No deployed build was tested.

Create a new file, readable and writable, with a creator SD. An existing path is an error:

struct peios_open_params p = {
    .desired_access = KACS_FILE_READ_DATA | KACS_FILE_WRITE_DATA,
    .disposition    = KACS_DISPOSITION_CREATE,    /* fail if the path exists */
    .sd             = creator_sd, .sd_len = creator_sd_len,
};

uint32_t status = 0;
int fd = peios_file_open(AT_FDCWD, "state.db", &p, &status);
if (fd < 0) { perror("open"); return -1; }

/* On success, status == KACS_STATUS_CREATED. */

To open an existing file, use KACS_DISPOSITION_OPEN with sd = NULL and sd_len = 0. If the create-only call returns EEXIST, decide whether accepting the existing object is appropriate before making that separate open call. The two calls are not an atomic create-or-open transaction; checking stat first does not remove the race either.

If parent inheritance is appropriate for a newly created file, OPEN_IF with sd = NULL and sd_len = 0 can report created versus opened in status_out without a separate stat. Only DIRECTORY and DELETE_ON_CLOSE create-option bits are accepted by the pinned kernel; put no-follow in flags. See the opening reference for the SDK fields.

Reading a file's security descriptor #

To see who can do what to a file, read its SD. secinfo selects which components you want — owner, group, DACL, SACL — so you fetch only what you need:

/* Probe, allocate, read (two-call). */
ssize_t need = peios_file_get_sd(AT_FDCWD, "state.db",
                                 KACS_SECINFO_OWNER | KACS_SECINFO_DACL,
                                 NULL, 0, 0);
void *sd = malloc(need);
peios_file_get_sd(AT_FDCWD, "state.db",
                  KACS_SECINFO_OWNER | KACS_SECINFO_DACL, sd, need, 0);

/* Parse it with a security.h view. */
peios_sd_view v;
peios_sd_parse(sd, need, &v);
peios_acl_view dacl;
if (peios_sd_view_dacl(&v, &dacl) == 0) {
    unsigned n = peios_acl_view_count(&dacl);
    /* iterate ACEs … */
}
free(sd);

If you already hold a file fd, use the fd-targeted peios_fd_get_sd instead of a path — no second path resolution, and for a normal file fd the check uses the access already baked in at open.

Changing a file's security descriptor #

Writing an SD is component-selective too: name the components you're changing in secinfo, and everything you don't name is preserved. To tighten a file's DACL without touching its owner or SACL:

/* Build an SD carrying only a DACL. */
peios_sd_builder *b = peios_sd_builder_new();
peios_sd_builder_dacl(b, new_acl, new_acl_len);
size_t sd_len; const void *sd_bytes = peios_sd_builder_bytes(b, &sd_len);

peios_file_set_sd(AT_FDCWD, "state.db", KACS_SECINFO_DACL, sd_bytes, sd_len, 0);
peios_sd_builder_free(b);

Because only KACS_SECINFO_DACL is selected, the owner, group, and SACL are left exactly as they were. The security.h builders are how you assemble the SD to apply.

Raw file-security interface #

The SDK helpers above wrap kacs_get_sd and kacs_set_sd. The following low-level details belong to programs implementing descriptor tools; shell users should use Managing file security. The generated KACS ABI is the source for syscall numbers and KACS_SECINFO_* constants; the *_SECURITY_INFORMATION names below are their specification spellings.

Raw access to security.peios.sd or system.ntfs_security is denied for reads, writes and removal. In particular, a raw read would expose the SACL without the component-specific ACCESS_SYSTEM_SECURITY gate. The syscall pair unifies access rules, validates descriptors and abstracts filesystem storage; see File Descriptor Storage.

kacs_get_sd #

The read syscall:

size = kacs_get_sd(dirfd, path, security_info, buf, buf_len, flags)

Returns the SD bytes for the requested components.

ParameterMeaning
dirfd, pathThe file to read. Standard dirfd-relative resolution.
security_infoA bitmask saying which components to return. See below.
buf, buf_lenOutput buffer.
flagsAT_EMPTY_PATH (use dirfd as fd-relative), AT_SYMLINK_NOFOLLOW (operate on the terminal symlink itself).

The kernel:

  1. Resolves the path.
  2. Checks the access rights corresponding to the requested components (covered below). Paths and O_PATH handles use a live AccessCheck; ordinary file descriptors use their cached granted mask.
  3. Reads the file's SD from its filesystem-native storage.
  4. Constructs a subset SD containing only the requested components. Other components have offset 0 in the header and the corresponding PRESENT bit is clear.
  5. Returns the subset SD in buf, with the total length as the return value.

Probe mode #

Calling with buf_len = 0 and buf = NULL is a probe — the kernel computes the size the SD would take and returns it without writing to the buffer. The probe returns the same size value that a non-probe call would; the caller can use this size to allocate exactly the right buffer.

The probe call always returns the size on success. It does not return -ERANGE — the probe is itself a question about size, and answering it is the kernel's job. -ERANGE would be the wrong signal for a deliberate probe.

A non-probe call with buf_len < required fails with -ERANGE without truncating the result. Probe again, allocate and retry; the size can change between calls. Do not assume an out-parameter receives the required size: this signature has none. The SDK two-call protocol describes the wrapper convention.

Access rules #

Different components need different rights:

Requested component (via security_info flag)Required right
OWNER_SECURITY_INFORMATIONREAD_CONTROL
GROUP_SECURITY_INFORMATIONREAD_CONTROL
DACL_SECURITY_INFORMATIONREAD_CONTROL
SACL_SECURITY_INFORMATIONACCESS_SYSTEM_SECURITY
LABEL_SECURITY_INFORMATIONREAD_CONTROL (the integrity label is in the SACL but its read is gated by READ_CONTROL, not ACCESS_SYSTEM_SECURITY)

(The numeric flag values are catalogued in Other constants.)

READ_CONTROL is the standard "read SD" right. Its implicit grant to the owner is subject to OWNER RIGHTS and the other access layers. ACCESS_SYSTEM_SECURITY is gated by SeSecurityPrivilege; an administrator label alone is not sufficient.

A caller asking for components they do not have rights for gets -EACCES. The component check is all-or-nothing: if any requested component fails its access check, the whole call fails rather than returning the permitted subset.

For combining requested components: just OR the flags. kacs_get_sd(..., OWNER | DACL, ...) returns owner and DACL but not SACL or group.

SACL and LABEL are mutually exclusive #

SACL_SECURITY_INFORMATION and LABEL_SECURITY_INFORMATION cannot be combined in one call. Setting both flags returns -EINVAL. The reasoning: LABEL_SECURITY_INFORMATION is a focused query for just the integrity label (which lives in the SACL); it has different access requirements than reading the full SACL. The kernel keeps the two paths separate.

kacs_set_sd #

The write syscall:

result = kacs_set_sd(dirfd, path, security_info, sd_buf, sd_len, flags)
ParameterMeaning
dirfd, pathTarget file.
security_infoWhich components to update.
sd_buf, sd_lenThe new SD bytes (self-relative format).
flagsAT_EMPTY_PATH, AT_SYMLINK_NOFOLLOW (operate on the terminal symlink itself).

The kernel:

  1. Resolves the path.
  2. Parses the SD blob. Rejects malformed SDs (size limit, bad ACL structure, etc.) with -EINVAL.
  3. Checks the required rights per the components being updated, using a live AccessCheck for a path or O_PATH handle and the cached granted mask for an ordinary file descriptor.
  4. Validates additional rules — owner SID is the caller's own or a SE_GROUP_OWNER group (unless SeRestorePrivilege), MANDATORY-flagged resource attributes are not removed (unless SeTcbPrivilege), integrity label is not raised above caller's own (unless SeRelabelPrivilege).
  5. Writes the SD to the file's native storage.
  6. Returns 0 on success.

Validation and component-right checks must succeed before the selected components are merged; unselected components are preserved. This is not a concurrent-update transaction. The Kernel TRM storage rules describe a last-writer-wins window: the xattr write and cache publication are not atomic with each other, although readers do not see a partially written descriptor. Coordinate writers rather than treating a read/edit/write sequence as compare-and-swap.

Access rules for writes #

ComponentRequired right
OwnerWRITE_OWNER (plus the owner SID validation)
GroupWRITE_OWNER
DACLWRITE_DAC
SACLACCESS_SYSTEM_SECURITY
LABEL (integrity label only)WRITE_OWNER (plus integrity constraint)

WRITE_DAC is the standard "modify DACL" right; the owner implicit grant is subject to OWNER RIGHTS suppression and the other access layers. WRITE_OWNER is needed to change the owner field (and the validation rules apply per Ownership). ACCESS_SYSTEM_SECURITY is the SACL gate.

The integrity label #

LABEL_SECURITY_INFORMATION (0x10) is the focused write path for setting just the integrity label. The label lives in the SACL as a SYSTEM_MANDATORY_LABEL_ACE, but setting it via this flag is treated as a separate operation from setting the full SACL — with different access requirements:

  • The right needed is WRITE_OWNER, not ACCESS_SYSTEM_SECURITY.
  • The caller cannot raise the integrity label above the calling token's own integrity level (without SeRelabelPrivilege).
  • Lowering the integrity label to at or below the caller's own integrity level is allowed.

LABEL_SECURITY_INFORMATION and SACL_SECURITY_INFORMATION cannot be combined in one call — same rule as for reading.

The use case: a process that wants to lower its files' integrity labels without holding SeSecurityPrivilege. The label is in the SACL conceptually, but setting it gets the WRITE_OWNER gate rather than the SACL gate, because adjusting the label down is a less sensitive operation than rewriting the audit policy.

SD parsing and validation #

The provided SD blob must be:

  • In self-relative format (SE_SELF_RELATIVE flag set in control bits).
  • Within the 65,535-byte size limit.
  • Internally consistent — the offsets in the header point to valid locations, the ACLs parse cleanly, the SIDs are well-formed.

Any failure of validation returns -EINVAL. The original SD on the file is unchanged.

Setting only some components #

The security_information flags tell the kernel which components of the provided SD to apply. A blob containing owner + DACL with only DACL_SECURITY_INFORMATION set updates only the DACL; the file's existing owner is preserved.

The blob structure must still be valid — components not being applied are typically absent (offset 0, PRESENT bit clear) in the blob, but the blob's header still needs to be a valid SD header. After the merge, the resulting descriptor must retain a non-null owner; its group may be null.

This is the pattern for updating one part of an SD without touching the others. Read the SD (probe + fetch), update the relevant component, write back with only that component's flag set.

Ownership transfer rules #

Setting the owner via kacs_set_sd triggers the rules described in Ownership:

  • The new owner must be the caller's own user_sid, or a SID in the caller's groups with SE_GROUP_OWNER set, or the caller must hold SeRestorePrivilege.
  • The operation must pass the WRITE_OWNER gate. SeTakeOwnershipPrivilege can supply that right through a live access check, subject to mandatory policy; possession does not bypass a cached fd mask or the new-owner SID restriction. See Ownership for availability and recovery limits.

A failed component-right check returns -EACCES; a failed owner-SID constraint without SeRestorePrivilege returns -EPERM.

The same rules apply whether you set ownership via OWNER_SECURITY_INFORMATION alone or in combination with other components.

File-security errors #

These are raw kernel error names; the SDK wrappers return -1 and set errno.

ErrorCause
-EACCESAccess check failed for the requested components.
-EINVALMalformed SD blob, invalid security_information combination, size limit exceeded, or other validation failure.
-EPERMOwner SID validation failed without SeRestorePrivilege, or integrity label too high without SeRelabelPrivilege, or attempted MANDATORY attribute removal without SeTcbPrivilege.
-ERANGE(kacs_get_sd only) Non-probe buffer too small; probe again and retry.
-ENOENTThe target path does not exist.
-EROFSThe selected path reaches the object through a read-only mount.

The Kernel TRM raw-call contract says AT_SYMLINK_NOFOLLOW on kacs_get_sd and kacs_set_sd selects the symlink object, unlike native open, which fails with ELOOP in that case. The flag descriptions above follow that kernel contract.

Important

The SDK query reference now records the pinned-source result: AT_SYMLINK_NOFOLLOW queries the terminal link's own SD, whereas native open rejects that link with ELOOP. libpeios forwards the query's flags. This resolves the former blanket no-follow query error; it does not establish that a link-SD write succeeds. The set-security description above retains its separate rights and storage requirements. No runtime test or historical release boundary was established.

For live-check versus cached-fd restore behavior, mandatory SACL attributes and full-SACL label constraints, see The Set-Security Interface. A full SACL write replaces the entire SACL. A label-only write preserves non-label ACEs; its input is either no SACL, to remove the explicit label, or a SACL containing exactly one non-inherit-only mandatory-label ACE.

Pre-flighting an open #

Sometimes you want to know whether a caller could open a file before you actually do. Read the file's SD, then run an access check against the caller's token with peios_file_generic_mapping — no open, no side effects, just the verdict.

Next #

  • file.h reference — every parameter, plus the fd-targeted calls and mount policy.
  • File access — the operator-side model of native file security.

Hardening a process

Peios / Developing for Peios / Access control

Peios lets a process opt into hardening — exploit mitigations enforced by the kernel on that process's security block. The SDK exposes this through a single call, peios_process_set_mitigations. This short guide covers using it well; the full flag set and semantics are in process.h and the operator docs.

Harden yourself at startup #

The common case is a program hardening itself early in main, before it processes any untrusted input:

#include <peios/process.h>

int main(void)
{
    /* Enforce W^X and speculation mitigation lock; abort on failure. */
    if (peios_process_set_mitigations(-1, KACS_MIT_WXP | KACS_MIT_SML) != 0) {
        perror("set_mitigations");
        return 1;   /* refuse to run unhardened */
    }
    /* ... the rest of the program runs with those protections on ... */
}

pidfd == -1 targets the calling process. mitigations is a mask of KACS_MIT_* bits (from <pkm/psb.h>); combine the ones you want and set them together.

Three things to know #

It's one-way. Mitigation bits can only be set, never cleared — once on, they stay on for the life of the process. That's the point: a mitigation you could turn off is one an attacker could turn off. Treat each call as a permanent commitment.

It's all-or-nothing, and fails closed. If any requested protection can't actually be activated, the call changes nothing and returns -1. You never end up believing a mitigation is on when it isn't. So request the set you require together and check the result once — success means the whole set is active. (Bits from earlier successful calls stay on regardless.)

Targeting another process is privileged. To harden a process other than your own, you need PROCESS_SET_INFORMATION on it and PIP dominance over it — you can't reach into a process you don't already dominate. For self-hardening (-1), neither is needed.

Choosing what to enable #

The bit catalogue and what each mitigation defends against is documented operator-side under process mitigations (and in the Peios Kernel TRM §3.3, the Process Security Block). A couple of notes for the SDK caller:

  • KACS_MIT_ALL is the mask of all valid bits — useful for validating input, not usually what you'd blanket-enable without thought.
  • KACS_MIT_CFI is a legacy alias that expands to KACS_MIT_CFIF | KACS_MIT_CFIB.

Enable the specific protections your program can tolerate, verify the call succeeded, and prefer to do it before you touch untrusted data.

Next #

  • process.h reference — the call in full.
  • Process mitigations — every mitigation and its threat model.

Peios Learn — documentation for the Peios project.

Built with Trail.