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

Linux compatibility

Single-page view · as markdown

Linux compatibility

Peios / Advanced Peios / Linux compatibility

Peios runs Linux applications with little to no modification. The Linux identity APIs — getuid, getgid, getgroups, setuid, setgid, capset, capget, the credential xattr, the credential-bearing socket controls — all continue to work. Many programs built against the Linux ABI need no changes; they call the same syscalls, see the same return values, and observe semantics close enough to Linux that nothing breaks. Some programs need adjustments; a few cannot work at all on Peios without restructuring.

The boundary between "works as-is" and "needs work" comes from a single principle: Peios does not carve out exceptions in the KACS model for Linux compatibility. The compatibility layer is built on top of KACS; it does not bend KACS to fit Linux. When a Linux API can be satisfied by deriving the answer from KACS state — getuid() returns a UID computed from the token — that works cleanly. When a Linux API expects to modify state that KACS owns — setuid() expects to change the kernel's notion of the calling process's identity — the compatibility layer either no-ops (preserving the syscall's contract without actually changing KACS state) or routes the request through KACS-aware mechanisms (and only succeeds when the calling token has the appropriate KACS privileges).

The result is best-effort compatibility. The values Linux APIs return are derived from the KACS model, not parallel to it. The token is the authoritative identity; the Linux credentials are a projection of it. The Linux compatibility layer is the bridge between "what Linux applications expect" and "how Peios actually works" — but it is a bridge that respects KACS at every point. Where a Linux semantic conflicts with KACS, KACS wins; the Linux call gets the closest reasonable approximation.

This page covers the model: how the projection works, what the Linux APIs see, and where the compatibility boundary lies.

The projection model in one sentence #

The token's identity is projected into Linux's UID/GID/capability fields, but only flows that direction — Linux APIs never write back to the token.

A process's primary identity is the user SID on its token. The kernel computes a corresponding UID (via the directory's SID-to-UID mapping) and stores it as part of the token's projection state. When the process calls getuid(), the value returned is the projection. The kernel does not consult the actual cred->uid field that Linux usually uses for this; it consults the token.

The same applies to GID and supplementary GIDs. They are projections from the token, not authoritative on their own.

This is the one-way rule. Token → cred always; cred → token never. There is no API in the compatibility layer that lets Linux semantics override the token. A setuid() call either causes the token to change (via authd, on the rare paths where that is allowed) or does nothing. It does not write a new UID into the token.

What you see vs what counts #

A Linux process running on Peios sees:

  • getuid(), getgid() return UID/GID values consistent with the token's projection.
  • getgroups() returns supplementary GIDs from the projection.
  • stat() on a file returns UID/GID values consistent with the file's owner SID (also projected).
  • /proc/<pid>/status shows the token's projected UIDs and capabilities.
  • capget() returns the mandatory capability substrate (covered in the capabilities page).

What the kernel actually uses for access decisions is the token, not what getuid() returns. If a process's token has user SID S-1-5-21-...-1001 and that SID projects to UID 1001, then getuid() returns 1001 — but the access check uses the SID. The two values are kept consistent by the projection, but the SID is authoritative.

Tools that work this way include: ls -l, ps, top, who, anything that uses standard POSIX APIs to query identity and ownership. They all see the projection; the underlying KACS state is what actually controls access.

What works, what changes #

A handful of Linux behaviours map cleanly into KACS; others are deliberately redirected. Quick summary:

Linux APIWhat Peios does
getuid, getgid, getgroupsReturns the projection from the token.
setuid, setgidNo-op without SeAssignPrimaryTokenPrivilege. With the privilege, becomes a full identity swap via authd.
setuid-on-exec (setuid bit)Cosmetic euid change without privilege; full token swap with privilege.
capget, capsetReturns the kernel's mandatory capability substrate; capset cannot clear ALLOW bits.
prctl(PR_CAPBSET_*)Operates on the capability substrate per the same rules.
setfacl / POSIX ACL xattrsUnconditionally denied — KACS replaces POSIX ACLs.
fchmod, chmodGated on WRITE_DAC: fchmod needs it in the fd's cached granted mask, chmod runs a fresh access check. The mode bits change, but FACS never consults them for access.
fchown, chownSame, gated on WRITE_OWNER. The Linux uid/gid changes, but the SD's owner SID does not — use kacs_set_sd for that.
setcap on files (file capabilities)Denied. Linux file capabilities are dead under KACS.
SO_PEERCRED, SCM_CREDENTIALSReturns projected UIDs (compat-only). Services needing real identity use kacs_open_peer_token.
getxattr/setxattr on security.peios.sd / system.ntfs_securityDenied; use kacs_get_sd / kacs_set_sd.
auditd and the Linux audit subsystemReplaced by KMES and eventd. The legacy audit records projected UIDs only.

The pattern: identity-querying APIs see the projection; identity-modifying APIs either no-op (preserving the Linux contract) or are redirected through KACS-aware paths.

Why the compatibility layer exists #

A reasonable question: why bother with Linux compatibility at all? Why not require applications to be rewritten?

The answer is operational: the ecosystem of Linux software is large. Forcing every application to be ported to a Peios-native API surface would be a barrier to adoption that the value of native integration does not justify. The compatibility layer lets most Linux software run with little or no modification.

The cost is the indirection of the projection — every getuid() consults the token rather than cred->uid, every setuid() is interpreted through the privilege model. The cost is small at runtime; the operational benefit is large.

The compatibility layer is not a translation layer. It does not "convert Linux access control to KACS"; KACS is what runs. The compatibility layer is the set of rules that make Linux APIs return sensible values when called on a system whose actual access control is KACS. Where a Linux behaviour cannot be satisfied without compromising KACS, the layer prefers approximation to special-casing — the Linux call returns something reasonable rather than the kernel making a KACS exception. This is why "best-effort" is the right framing: most things work, some things approximate, a few things require the application to be aware of the underlying model.

DAC neutralisation #

A specific aspect of the compatibility layer worth flagging: the kernel sets every process up with a mandatory set of Linux capabilities — CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_FOWNER, CAP_CHOWN, CAP_SETUID, CAP_SETGID — that effectively neutralise the legacy Linux DAC checks.

The reason: Linux's traditional DAC (file mode bits, UID/GID checks) would otherwise run before the KACS LSM hooks. A Linux file mode of 400 (read-only by owner) would block a write attempt by a non-owner before KACS could evaluate the DACL. The mandatory capabilities tell the Linux DAC to defer to the LSM layer; KACS then makes the authoritative decision.

This is why the file's mode bits don't enforce access in Peios — DAC is neutralised. The mode is informational (derived from the SD); KACS is the authority.

The capability story is more nuanced than just "neutralise DAC"; it's covered in detail in DAC neutralisation and capabilities.

What about the root user #

A common question: what about root? Linux's root is UID 0; programs that check geteuid() == 0 to detect administrative privilege rely on this. On Peios, who is "root"?

The mapping is:

  • The SYSTEM principal (S-1-5-18) projects to UID 0. Processes running on the SYSTEM token see getuid() return 0.
  • A user in BUILTIN\Administrators (S-1-5-32-544) projects to their normal UID, typically non-zero. They are administratively privileged in KACS terms but getuid() does not return 0.
  • The uid0 utility lets a process with SeAssignPrimaryTokenPrivilege and the right authority cosmetically set its UID to 0 without changing the underlying token. Useful for legacy applications that hard-code geteuid() == 0 checks but where the actual identity should remain non-SYSTEM.

The uid0 mechanism is covered in setuid and uid0. The short version: legacy "is this root?" checks see UID 0 when the calling code has explicitly arranged for them to; otherwise the real projection is what they see.

Where to start #

If you want the projection mechanism in detail — how the token's identity becomes UID/GID values, how the projection handles impersonation, how the kernel maintains consistency — read Credential projection.

If you want the capability story — DAC neutralisation, the 41 Linux capabilities classified, why security_capable() is authoritative — read DAC neutralisation and capabilities.

If you want setuid semantics and the uid0 utility, read setuid and uid0.

If you want peer credentials — SO_PEERCRED, SCM_CREDENTIALS, the rules for migrating Linux services to KACS-aware peer-identity APIs — read Peer credentials.

If you want the Linux features that survive only as relics — superseded mechanisms that still work but sit outside Peios's recommended surface, and where to look instead — read Linux relics.

For the merged filesystem used to combine packaged and local directory trees — now covered with the rest of the storage tooling — read StrataFS.

Credential projection

Peios / Advanced Peios / Linux compatibility

The projection model is straightforward: when authd mints a token, it resolves the user's SID against the directory's SID-to-UID mapping and stores the resulting UID, GID, and supplementary GIDs on the token. These values are the projection — derived from the SID, not parallel to it. From then on, any Linux API that asks "what's this process's UID?" gets the answer from the projection.

The projection is one-way. Token state determines the projection; the projection never determines token state. There is no API that lets a Linux call modify the token through the projection layer; the token is authoritative.

This page covers how the projection is constructed, what Linux APIs see when they consult it, how impersonation changes the visible projection, and the rule that keeps the model consistent.

What's on the token #

Every token carries three projection-related fields:

FieldMeaning
projected_uidThe Linux UID corresponding to the token's user SID. 65534 if the SID has no mapping.
projected_gidThe Linux GID corresponding to the token's primary group SID. Same fallback.
projected_supplementary_gidsAn array of GIDs for the supplementary groups on the token.

These fields are set when the token is created. authd reads the user's SID-to-UID mapping from the directory at authentication time, fills in the projection, and includes the values in the wire-format token specification passed to kacs_create_token.

The mapping is per-directory. On a standalone Peios system, loregd's registry holds the mapping; on a domain-joined system, the domain's directory provides it. The same SID can in principle map to different UIDs on different systems if the directories are different, though in practice deployments aim for consistency.

A SID with no mapping (a principal that exists in the directory but has no UID assignment) projects to 65534 — the conventional Linux "nobody" UID. This lets the projection produce a value even when the directory does not have one, while signalling that the principal is unknown to the projection.

What syscalls see #

The standard Linux identity syscalls all return projected values:

SyscallReturns
getuid()The primary token's projected_uid.
geteuid()The effective token's projected_uid — primary if not impersonating, impersonation token's value if impersonating.
getgid()The primary token's projected_gid.
getegid()The effective token's projected_gid.
getresuid(&r, &e, &s)Same: r and s from primary, e from effective.
getresgid(&r, &e, &s)Same.
getgroups()The effective token's projected_supplementary_gids.
getlogin() (via libc)Reads /proc/self/status which reads the primary token's projection.
stat(), fstat(), lstat()File's owner SID projected to UID, primary group SID projected to GID.
/proc/<pid>/statusThe primary and effective token's projected fields.
/proc/<pid>/loginuidThe primary token's projected UID.

The split between getuid (primary) and geteuid (effective) preserves Linux semantics where the two diverge during impersonation-like operations. On Peios, the divergence is when the thread is actually impersonating.

The kernel maintains the synchronisation between token state and these queries automatically. Adjusting privileges, adjusting groups, installing/reverting impersonation — each updates whatever the next getuid()-style call would see.

What impersonation does #

A thread that is currently impersonating has two tokens: its primary and its impersonation. The projection layer follows the effective token (the impersonation, while it's in effect) for geteuid-style queries and the primary for getuid-style queries.

The result:

  • Before impersonating: getuid() == geteuid() == projected_uid(primary).
  • During impersonation: getuid() == projected_uid(primary), geteuid() == projected_uid(impersonation).
  • After reverting: back to the first state.

A program checking "am I running as user X right now?" via geteuid() sees the impersonated user, not the service's own user. A program checking "what's my real identity?" via getuid() sees the service's own user.

This is the standard Linux divergence: real (primary) vs effective (current). Peios preserves it through the projection. A service that captures a client's identity via impersonation and then opens a file as the client sees the file open with the client's projection (which is what allows the open to succeed if the client has access); other Linux calls on the same thread see the same effective identity.

The threads that are not impersonating see only the primary projection. Impersonation is per-thread; the projection is too.

current_fsuid() and the fsuid path #

A subtle case: the Linux kernel internally uses a function called current_fsuid() (and the corresponding current_fsgid()) for file-related credential lookups — file ownership, disk quotas, keyring lookups, NFS credentials. These are different from getuid()/geteuid(); they go through their own code path inside the kernel.

Peios patches current_fsuid() (and its variants) to return the projected UID from the effective token, not from cred->fsuid. This is what makes the projection consistent across all the places the kernel might consult it.

The practical effect: a thread impersonating a client opens a file. The Linux kernel might internally call current_fsuid() to record who created the file in the filesystem's metadata. The patched current_fsuid() returns the impersonated client's UID, so the file appears to be created by the client. This is consistent with the file's owner SID being the client's SID (which KACS would set), and consistent with stat() later returning the client's UID.

Without the patch, current_fsuid() would return cred->fsuid, which might be the service's own UID — and the file's metadata would be inconsistent with the file's actual KACS owner.

The one-way rule #

The projection flows token → cred. Never cred → token.

A Linux call that would, on a non-Peios system, modify the kernel's notion of the process's UID is not allowed to modify the token. Specifically:

  • setuid() and friends do not modify the token. They either become a no-op (without SeAssignPrimaryTokenPrivilege) or trigger a full identity swap through authd (with the privilege). See setuid and uid0.
  • The setuid-on-exec bit (S_ISUID in a file's mode) similarly does not modify the token without the privilege.
  • prctl(PR_SET_SECUREBITS) and related operations do not affect the token's projection.

The reason: the token is the authoritative identity. Letting Linux APIs write to it would mean the token's value depends on what Linux code thinks. Peios's model is that the token is decided by authd (at mint time) and adjusted only through KACS APIs (AdjustPrivileges, AdjustGroups, etc.). Linux is a consumer; it does not get to be a producer.

File ownership in stat() #

When stat() returns a file's owner and group, the values come from the file's SD — specifically, the owner SID's projection and the primary group SID's projection.

The flow:

  1. The file has an SD with owner_sid = S-1-5-21-...-1001 and group_sid = S-1-5-21-...-513.
  2. Each SID is run through the SID-to-UID mapping.
  3. stat() returns st_uid = 1001, st_gid = 513.

The values are computed from the SD's SIDs every time, not stored as separate fields. The file's underlying filesystem may have its own i_uid and i_gid fields (ext4 does), but the kernel maintains them consistent with the SD-derived values. A change to the file's owner SID via kacs_set_sd updates the SD and re-derives the projected UIDs, which then become visible to stat().

This consistency is what makes ls -l work. The output shows ownership; the ownership is the projection of the SD; the SD is what KACS evaluates against. The three views agree.

SO_PEERCRED and SCM_CREDENTIALS #

Two Linux APIs let one process learn another's identity over Unix sockets:

  • SO_PEERCRED — getsockopt option returning the peer's pid, uid, gid.
  • SCM_CREDENTIALS — sendmsg/recvmsg control message allowing the sender to attach (or the receiver to extract) similar credentials.

On Peios, these return the peer's projected UID/GID values. The values are correct in the projection sense — they correspond to the peer's token's projection. But they are not the right tool for security-relevant identity:

  • They don't distinguish between an authenticated user and an unauthenticated process running as the same UID.
  • They don't carry the token's SIDs, groups, integrity level, or privileges.
  • They don't reflect impersonation correctly in all cases.

For security purposes, the right tool is kacs_open_peer_token — it returns a token fd carrying the full identity. See Peer credentials.

SO_PEERCRED and SCM_CREDENTIALS are kept for compatibility with Linux applications that use them for non-security purposes (logging, debugging, friendly identification). Code that needs to make access decisions on peer identity uses the KACS-aware API.

What the projection is not #

A few clarifications:

  • The projection is not the access decision input. KACS uses the token (SIDs, groups, privileges, integrity, PIP); the projection is for Linux API compatibility only. AccessCheck does not consult projected_uid.
  • The projection is not historical. A token reflects identity now; if the SID-to-UID mapping changes (rare but possible), existing tokens continue to use their cached projection. New tokens (created after the mapping change) would see the new value. The projection is fixed when the token is minted.
  • The projection is not the file system's notion of ownership. ext4 stores i_uid and i_gid; Peios keeps these consistent with the SD's owner-projection, but the SD is authoritative. A file whose i_uid somehow diverges from the SD owner's projection is inconsistent; KACS uses the SD.
  • The projection is not a substitute for the token. Code that needs to make security decisions on identity should use the token, not the projection. The projection is for compatibility; the token is for authority.

The cleanest mental model: the projection is the Linux-shaped view of an identity that fundamentally lives in KACS. It exists to satisfy POSIX programs that ask "what's my UID?" and have to get a number back. The number is computed; the actual identity is something else.

Where to go next #

For how Linux's own DAC and capability checks are made to defer to KACS, read DAC neutralisation and capabilities.

For what the setuid family does — and does not do — to the projection, read setuid and uid0.

For the security-grade replacement for projected peer credentials, read Peer credentials.

DAC neutralisation and capabilities

Peios / Advanced Peios / Linux compatibility

Linux has its own access-control mechanisms below the LSM layer — DAC (file mode bits, owner/group checks) and capabilities (POSIX-style fine-grained privileges). When Peios's KACS runs as an LSM, it sees an access after these other layers have already had their say. If DAC has already refused the access for legacy reasons, KACS never gets a chance to evaluate.

The fix is DAC neutralisation — Peios sets up every process with a specific set of mandatory Linux capabilities that effectively defer the DAC and capability decisions to the LSM layer. Combined with a classification of the 41 standard Linux capabilities (some always-on, some mapped to KACS privileges, some always-off), this lets KACS be the authoritative access-decision layer.

This page covers the model — what DAC neutralisation is, how it works, and the three-way classification of Linux capabilities.

The problem: DAC runs first #

In a standard Linux kernel, an access proceeds through several layers:

  1. The syscall (open, read, etc.) is called.
  2. The kernel does its DAC check — file mode bits, owner UID, group GID. If DAC refuses, the syscall fails with EACCES.
  3. The kernel does capability checks for operations that require specific capabilities.
  4. LSM hooks fire. SELinux, AppArmor, etc. get to make additional decisions.

KACS is an LSM. It fires at step 4. By the time it runs, DAC has already had its say. If a file has mode 400 (read-only by owner), a non-owner attempting to read it gets EACCES from DAC before KACS sees the call. KACS may have a DACL granting the access, but it cannot help — DAC already refused.

The same applies to capabilities. Operations gated by Linux capabilities (CAP_SYS_ADMIN, CAP_NET_BIND_SERVICE, etc.) check the capability at the relevant point. If the process lacks the capability, the operation fails. KACS's privilege model (SeBindPrivilegedPortPrivilege, SeTcbPrivilege, etc.) is parallel but separate.

The naive answer would be to remove DAC from the kernel. But that breaks Linux compatibility — applications expect the kernel to enforce mode bits. Peios's answer is more subtle: keep DAC and capabilities, but neutralise them so they always defer to LSM.

The fix: mandatory capabilities #

Every process on a Peios system is set up with a mandatory set of Linux capabilities always present in their effective set:

  • CAP_DAC_OVERRIDE — bypass DAC read/write checks.
  • CAP_DAC_READ_SEARCH — bypass DAC read/search checks on directories.
  • CAP_FOWNER — bypass ownership-based permission checks.
  • CAP_CHOWN — bypass restrictions on chown.
  • CAP_SETUID — bypass restrictions on setuid.
  • CAP_SETGID — bypass restrictions on setgid.

These caps are present on every process's credentials, mandatorily, regardless of what the application's binary or the user requested. They cannot be removed by capset().

Their effect: the DAC layer of the kernel sees these caps and treats the corresponding checks as already-passed. The check proceeds to the LSM layer, where KACS gets to make the actual decision.

This is the "neutralisation". DAC is not gone — the code paths still exist — but it never refuses anything by itself. Every access flows through to KACS.

The same approach is taken for the capability checks: capabilities relevant to access decisions are pre-allowed so the kernel does not stop the operation before LSM gets to see it. KACS then makes the call.

The capability classification #

There are 41 Linux capabilities defined in v0.20 (the standard set from Linux 5.x). Peios classifies each as one of three things:

ClassMeaning
ALLOWAlways present in the process's effective set. Cannot be cleared. Used to neutralise the DAC and capability checks that need to defer to LSM.
PRIVILEGEMapped to a KACS privilege. cap_capable() returns "granted" iff the calling token holds the corresponding KACS privilege.
DENYPermanently denied. The check always says "no", regardless of the credential state. Used for capabilities that have no useful semantic on Peios.

The classification is per-capability and is hard-coded into the kernel.

ALLOW capabilities #

The DAC-neutralisation set is the core of ALLOW:

CapabilityWhat it would normally gateWhy ALLOW
CAP_DAC_OVERRIDEDAC mode-bit checksKACS replaces DAC entirely; the bit must be on so DAC always defers.
CAP_DAC_READ_SEARCHDAC read/search on directoriesSame.
CAP_FOWNEROwner-based bypasses (chmod own files, etc.)Same.
CAP_CHOWNRestriction on chown()Linux normally requires CAP_CHOWN to chown; Peios redirects chown through KACS anyway.
CAP_SETUIDRestriction on setuid()setuid() is itself reinterpreted by Peios; the cap must be present for the syscall to even get to the LSM hook.
CAP_SETGIDSame for setgid()Same.

These are mandatory. No process can clear them; capset() ignores attempts to remove them.

PRIVILEGE capabilities #

Many Linux capabilities map cleanly to a KACS privilege. Examples:

CapabilityMaps to KACS privilege
CAP_NET_ADMIN(administrative network operations, gated by appropriate KACS privileges in v0.20)
CAP_SYS_TIMESeSystemtimePrivilege
CAP_SYS_BOOTSeShutdownPrivilege
CAP_SYS_NICESeIncreaseBasePriorityPrivilege
CAP_IPC_LOCKSeLockMemoryPrivilege
CAP_SYS_RESOURCESeIncreaseQuotaPrivilege
CAP_NET_BIND_SERVICESeBindPrivilegedPortPrivilege (Peios-custom)
CAP_AUDIT_CONTROL, CAP_AUDIT_READ, CAP_MAC_ADMINSeSecurityPrivilege
CAP_PERFMONSeSystemProfilePrivilege OR SeProfileSingleProcessPrivilege OR SeLoadDriverPrivilege (OR-mapped — see below)

OR-mapping. Most capabilities map to a single KACS privilege. A small number span multiple Peios privilege tiers, where no single privilege covers everything the Linux capability gates — these OR-map: security_capable() returns granted if the token holds any of the listed privileges. CAP_PERFMON is the current example. The cap_capable() answer is only a ceiling that prevents false denials; the specific privilege for the specific operation is enforced at the relevant syscall hook. For perf, the perf_event_open path distinguishes own-task profiling (no privilege), cross-task profiling (SeProfileSingleProcessPrivilege + PIP dominance), and system-wide profiling (SeSystemProfilePrivilege). OR-mapping never grants authority the holder does not already have via one of the listed privileges.

When the kernel asks "does this caller have CAP_X?", the answer is computed by consulting the token's privileges. The kernel's security_capable() hook is what does this lookup — Peios's PKM module overrides the default capability check to consult KACS privileges instead of (or in addition to) the credential's capability set.

This means a process's Linux-capability state and its KACS-privilege state are kept consistent for the PRIVILEGE-class capabilities. A user with SeSystemtimePrivilege enabled on their token gets the answer "yes" from cap_capable(CAP_SYS_TIME); a user without it gets "no".

DENY capabilities #

A few Linux capabilities have no useful semantics on Peios and are always denied:

CapabilityWhy DENY
CAP_SETFCAPLinux file capabilities (security.capability xattr) are dead on Peios. The xattr can't be set; there's nothing for this capability to do.
Reserved / future capabilitiesCaps the kernel has defined but Peios has no mapping for, default to DENY.

These are denied unconditionally. No combination of token state grants them.

security_capable() is authoritative #

The kernel exposes a function security_capable() that callers use to check "is this caller allowed to do the thing this capability gates?". Peios's PKM module hooks this function:

  • For ALLOW caps: always returns granted.
  • For PRIVILEGE caps: returns granted iff the corresponding KACS privilege is enabled on the calling token.
  • For DENY caps: always returns denied.

security_capable() is authoritative. The actual bit state of the credentials (the cap masks visible to capget()) is informational; the security decision comes from security_capable().

Why this matters: a program that reads /proc/self/status sees a capability bitmask. That bitmask is the "what the credentials currently say" view — for compatibility with tools that parse the file. But the kernel's enforcement uses security_capable(), which goes through KACS.

The two views are kept consistent for ALLOW (the bits are always on) and DENY (the bits are always off). For PRIVILEGE, the visible bitmask reflects the KACS privilege state — a privilege enabled on the token corresponds to the cap appearing in the bitmask.

capset() limitations #

capset() lets a process modify its capability set. Under Peios:

  • It cannot clear ALLOW bits. Attempts to remove CAP_DAC_OVERRIDE etc. silently leave them set. The process cannot opt out of DAC neutralisation.
  • It cannot grant DENY bits. Attempts to add CAP_SETFCAP etc. silently fail (the bit stays clear).
  • For PRIVILEGE bits, capset() cannot grant capabilities the corresponding KACS privilege does not provide. A process can drop the bit (which would normally drop the capability), but this does not affect the underlying KACS privilege — the privilege is the source of truth.

The kernel maintains the apparent capability state consistent with the KACS privileges (and the ALLOW/DENY rules). A program using capset() to try to drop privileges sees the visible mask drop, but the underlying KACS state is what security_capable() consults.

Warning

This is intentional: programs that drop capabilities for security hardening (the standard "drop CAP_NET_ADMIN before processing untrusted input" pattern) do not get the semantics they expect, because the kernel's authoritative decision uses KACS. The drop appears to succeed but does not take effect. To actually drop authority on Peios, use AdjustPrivileges to remove the corresponding KACS privilege.

prctl(PR_SET_KEEPCAPS) and the secure bits #

A program that does setuid and wants to preserve some capabilities afterward sets PR_SET_KEEPCAPS via prctl(). Under Peios:

  • prctl(PR_SET_KEEPCAPS, 1) is accepted but does not change the effective behaviour. The Linux capability state is reset across setuid by default; the KACS token state is unaffected by setuid (unless SeAssignPrimaryTokenPrivilege triggers the full identity swap path).
  • prctl(PR_SET_SECUREBITS, ...) is similarly accepted but does not modify the KACS state.

These are operations that affect the Linux credentials view. The KACS authority continues to live on the token.

Capabilities at exec — file capabilities are dead #

Linux supports file-bearing capabilities via the security.capability xattr. A binary with this xattr gets the listed capabilities at exec, regardless of who launched it. This is how (for example) ping gets CAP_NET_RAW on a Linux system.

Under Peios, file capabilities are dead:

  • Writing the security.capability xattr is unconditionally denied.
  • Existing security.capability xattrs on files are ignored at exec.
  • The mechanism's role on Peios is filled by KACS privileges, applied at token creation time by authd.

A binary that needs special capabilities on Peios doesn't carry them as a file xattr. Instead, authd's privilege policy decides which principals get which privileges; the binary running under one of those principals' tokens gets the corresponding KACS privileges naturally.

The capability CAP_SETFCAP (set file capabilities) is in the DENY class as a consequence — there are no file capabilities to set.

What this looks like in practice #

For an application running on Peios:

  • Linux DAC checks are invisible. The application's mode-bit-aware code paths work, but the mode bits don't refuse anything. KACS makes the call.
  • Capability checks (via security_capable()) return the KACS-derived answer. A process that holds SeSystemtimePrivilege on its token sees CAP_SYS_TIME as granted; one that doesn't sees it as denied.
  • capset() does not grant or remove ALLOW/DENY capabilities. The mask returned by capget() is informational.
  • File capabilities don't work. Binaries that depended on security.capability need to be run under tokens with the appropriate KACS privileges instead.

For most applications this is transparent. They make their normal calls; the access decisions come out as KACS decides. The exceptions are programs that explicitly manipulate Linux capabilities — a paranoid daemon that calls prctl(PR_CAPBSET_DROP) to drop unneeded capabilities — and these programs see their drops not take effect in the way they expect. Migrating to use KACS privileges via AdjustPrivileges is the right pattern.

Where to go next #

For the projection that feeds getuid and friends, read Credential projection.

For the setuid family's reinterpretation, read setuid and uid0.

For the KACS privileges the PRIVILEGE-class capabilities map to, read Privileges.

setuid and uid0

Peios / Advanced Peios / Linux compatibility

The Linux setuid() family of syscalls — setuid, seteuid, setreuid, setresuid, plus the GID variants — is reinterpreted under Peios. Without a specific privilege, calling setuid() succeeds (returns 0) but does not actually change anything. With the privilege, it triggers a full identity swap through authd. The setuid-bit-on-exec mechanism (S_ISUID in a file's mode) follows the same rule.

For legacy programs that check getuid() == 0 to detect administrative privilege, a separate utility — uid0 — provides a cosmetic UID-0 view without actually changing the underlying token. This page covers all three: setuid syscalls, the setuid bit, and uid0.

The setuid syscalls #

The Linux setuid family has several variants:

  • setuid(uid_t uid) — set real and effective UIDs to uid.
  • seteuid(uid_t euid) — set effective UID to euid.
  • setreuid(uid_t ruid, uid_t euid) — set real and effective independently.
  • setresuid(uid_t ruid, uid_t euid, uid_t suid) — set real, effective, and saved-set independently.
  • Corresponding setgid, setegid, setregid, setresgid for GIDs.

On Linux, these change the kernel's cred->uid (and related fields). The credential is updated; the next getuid() returns the new value.

On Peios, the behaviour depends on whether the calling token holds SeAssignPrimaryTokenPrivilege:

Caller has SeAssignPrimaryTokenPrivilegeBehaviour
NoThe syscall returns 0 but nothing changes. The token is unchanged. The projection is unchanged. getuid() returns the same value before and after.
YesThe kernel makes an upcall to authd to perform a full identity swap. authd authenticates the new identity (or constructs the appropriate token), and the calling process's primary token is replaced with the new one.

The no-op path is the default. The privileged path is reserved for a narrow set of components — typically authd itself, plus a small number of bootstrap services.

Why no-op without privilege #

A reasonable question: why does setuid succeed but do nothing? Why not just return an error?

The answer is compatibility. Linux programs assume setuid() works in the documented way. A program that does setuid(geteuid()) to "drop" privileges expects success. A setuid(0) followed by setuid(non-root) is a common idiom for "elevate, do work, drop". If setuid simply returned an error, every Linux program that did this would fail on Peios.

By making the call succeed but not change anything, Peios preserves the contract — programs see success — without actually changing the token. The programs continue running on whatever identity they had; the kernel's KACS-level access control is unaffected.

The cost: programs that rely on the post-setuid identity having changed might do incorrect things. A program that drops to a less-privileged UID expecting that to limit it will find that KACS access checks are unaffected, and the process retains whatever access its token had. For most programs this is fine (they're using setuid as defence in depth, and KACS's gates are stricter); for a few it could be surprising.

The escape hatch: a program that genuinely needs to change identity at runtime needs to be running with SeAssignPrimaryTokenPrivilege (so the call actually does something) or to be re-architected to do the identity-swap work via the proper KACS channels (call authd directly, get a token, install it via KACS_IOC_INSTALL).

What the privileged path does #

When the calling token holds SeAssignPrimaryTokenPrivilege and setuid(N) is called, the kernel:

  1. Looks up the SID corresponding to UID N (via the directory's reverse mapping).
  2. Asks authd to construct a token for that principal.
  3. Replaces the calling process's primary token with the new one.
  4. Updates the credentials so subsequent getuid() returns N.

The token is genuinely different now. Privileges, groups, integrity level are whatever authd produced for that principal. The previous token is released (its references drop).

This is what login frontends do. A user signs in; the login frontend (running with SeAssignPrimaryTokenPrivilege) calls setuid(target_user); the frontend's token is replaced with the user's token; the rest of the user's session runs as the user.

This is also the path for su and similar utilities — they run with the privilege and use the setuid syscall to actually become a different user.

The privilege is rare #

SeAssignPrimaryTokenPrivilege is held by:

  • authd itself (which actually needs it to mint tokens).
  • Login frontends that handle user sign-in (sshd, the console login, terminal services).
  • A few specific TCB tools that need to launch processes as specific users (peinit at boot, certain administrative utilities).

Ordinary programs do not have it. A user-installed application that calls setuid() falls through to the no-op path.

This is the right distribution: only components that legitimately swap identities have the privilege. Everything else gets the compatibility behaviour.

The setuid bit on exec #

A file with S_ISUID set in its mode runs as the file's owner when exec'd (on Linux). The setuid bit is what makes ping run as root, makes passwd able to modify /etc/shadow, makes sudo work.

On Peios, the setuid bit's behaviour mirrors the setuid syscall:

Calling token has SeAssignPrimaryTokenPrivilegeSetuid-bit behaviour
NoThe euid/suid fields are cosmetically updated to match the binary's owner UID, but the KACS token is unchanged. The binary runs as the calling principal, just with a different getuid() return.
YesA full identity swap occurs at exec, as if setuid(file_owner) had been called between fork and exec.

The cosmetic-only path is what most setuid-bit binaries get on Peios. The binary runs as the user who invoked it; the euid that geteuid() returns is the binary's owner; KACS sees no identity change.

For most uses this is correct. A setuid-root binary on a Linux system that just wants to do "is the caller root?" check sees geteuid() == 0 even on Peios; the cosmetic euid update is enough for that.

For uses that actually need to perform privileged operations on the user's behalf, the cosmetic-only path is insufficient. The KACS access control sees the calling user, not the binary's owner. The binary fails its access checks. The fix is to either:

  • Run the binary as a service launched by peinit with the appropriate token.
  • Have the binary connect to a service (running with the appropriate identity) and ask the service to do the work.

This is the pattern for "privileged operations" on Peios. Rather than setuid-bit binaries, services run with the right token; clients connect to the services and ask for what they need.

The setuid-bit-as-identity-swap path (with SeAssignPrimaryTokenPrivilege) exists for compatibility with tooling that genuinely needs it — but it requires the parent process to hold the privilege.

uid0 — cosmetic root for legacy programs #

A specific class of legacy programs hard-code getuid() == 0 (or geteuid() == 0) checks to detect root and refuse to run otherwise. The programs may have legitimate logic that requires elevated privileges, or they may just be checking out of paranoia.

For these programs, Peios provides the uid0 utility. It is a small wrapper that:

  1. Runs as a user with SeAssignPrimaryTokenPrivilege (or chains through one).
  2. Sets the calling credentials' cred->uid, cred->euid, and cred->suid all to 0.
  3. Execs the target binary.

The result: the target binary sees getuid() == 0, satisfies its root check, and proceeds.

Crucially, uid0 does not change the KACS token. The current_fsuid() patch (from Credential projection) ensures that file-related credential lookups still return the projected UID from the token — not the cosmetic 0. So uid0 is purely a cosmetic adjustment for legacy "am I root?" checks; it doesn't grant any actual privileges or change the security-relevant identity.

The use case: a legacy script that does [ $(id -u) -eq 0 ] || exit 1 at the top. The script's actual work might be perfectly fine to run as the calling user, but the check refuses. uid0 makes the check pass.

The uid0 utility itself is signed and runs with the appropriate privilege; an ordinary user invoking uid0 does not gain root authority — they gain a cosmetic UID-0 view for the duration of the wrapped program. The KACS token is unchanged; the access decisions still flow through KACS.

This is the right way to handle the "is root?" check pattern. The check is satisfied; the actual authority comes from KACS; the legacy code path works.

Comparison: setuid syscall, setuid bit, uid0 #

A handy summary:

MechanismCaller needsEffect on tokenEffect on getuid() / geteuid()
setuid(N) without privilege(none)UnchangedUnchanged (call returns 0 but doesn't actually set anything)
setuid(N) with privilegeSeAssignPrimaryTokenPrivilegeFull swap via authdReflects the new identity
exec of setuid-bit binary without privilege(none)Unchangedeuid/suid cosmetically updated to binary owner; uid unchanged
exec of setuid-bit binary with privilegeSeAssignPrimaryTokenPrivilegeFull swap to binary ownerReflects new identity
uid0 wrapperThe wrapper itself runs with the privilegeUnchangedCosmetic uid/euid/suid all = 0; current_fsuid() still returns projected UID

The pattern: real identity changes are gated by the privilege and go through authd. Cosmetic changes (for legacy compatibility) don't require the privilege and don't touch the token.

What setuid semantics are not #

A few clarifications:

  • They are not a way to elevate privilege. A program calling setuid(0) without the privilege does not gain root authority — the call is a no-op. To genuinely gain authority, you need a different token, which requires authd.
  • They are not the way Peios changes identity. The setuid syscall is a compatibility layer. The native way to change identity is to be assigned a different token by authd (typically through a re-authentication or through being launched with a specific token by peinit).
  • They are not bypass mechanisms for KACS. Whatever the setuid syscall does, the KACS access checks continue to operate against the token. There is no setuid combination that gets around a DACL.
  • They are not how login frontends actually work. Login frontends do use setuid() (with the privilege), but the actual identity-establishment work is in authd — minting the token, setting up the session, applying the privileges and claims. The setuid() call is the final step that installs the result on the calling process.

The cleanest mental model: setuid is a Linux-compatibility veneer on the actual Peios identity machinery. The veneer is enough for legacy programs to think they're operating on Linux; the actual identity changes (when they happen) go through authd.

Migrating away from setuid #

For new code or refactored services, the cleaner pattern is to avoid setuid entirely:

  • Services should be launched with the right token from the start. peinit fork-installs the correct token before exec; the service never needs to setuid.
  • User-facing operations should be done as the user, not via setuid-to-root. Impersonation (capturing the user's token via peer-token capture) is the right pattern — the service can act on the user's behalf without changing its own identity.
  • Cross-identity work happens through IPC. A service needing to do something as another identity sends an IPC request to a service running with that identity; the latter does the work.

Setuid is the legacy compatibility path. Native Peios code paths look different.

Where to go next #

For the projection behind the cosmetic UID values, read Credential projection.

For why setuid's Linux-level checks defer to KACS in the first place, read DAC neutralisation and capabilities.

For acting on another identity without changing your own, read Peer credentials.

Peer credentials

Peios / Advanced Peios / Linux compatibility

When one process connects to another over a Unix socket, the recipient often wants to know who is connecting. Linux provides two mechanisms for this — SO_PEERCRED (a socket option) and SCM_CREDENTIALS (a control message). Both return basic credential information about the peer.

On Peios, both continue to work and return projected UID/GID values — useful for compatibility with Linux applications, but insufficient for making security decisions. For services that need to authenticate the connecting peer's identity to decide access, the right tool is kacs_open_peer_token, which returns a full KACS token reflecting the peer's complete identity.

This page covers the two Linux mechanisms and the right replacement.

SO_PEERCRED #

SO_PEERCRED is a getsockopt option on a connected Unix socket. The caller queries the socket and gets back a struct ucred:

struct ucred {
    pid_t pid;
    uid_t uid;
    gid_t gid;
};

The fields are:

  • pid — the connecting peer's PID at connect time.
  • uid — the projected UID of the peer's effective token at connect time.
  • gid — the projected GID similarly.

On Peios, the projection is computed from the token at the moment the connection was made. If the peer was impersonating at connect, the projected UID is the impersonated client's. If not, it's the peer's own.

The values are stable for the life of the socket. If the peer changes identity later (impersonation install/revert, token adjustment), the values returned by SO_PEERCRED do not update.

What SO_PEERCRED does not capture #

The struct ucred is fundamentally lossy. It returns three numbers — pid, uid, gid. It does not return:

  • Group memberships beyond the primary GID.
  • Privileges.
  • Integrity level.
  • PIP fields.
  • Claims (user or device).
  • The session ID.
  • Whether the connecting principal is authenticated, anonymous, or somewhere in between.
  • The token's restricted-SID list, confinement state, audit policy, or any other non-trivial structure.

For most security purposes, the missing information matters more than what's present. A service that grants access based on UID does not distinguish "user 1001 connecting as their normal Medium-integrity session" from "user 1001 connecting from a Low-integrity sandbox where they shouldn't be doing this". The UID is the same; KACS treats the two cases differently; SO_PEERCRED cannot tell them apart.

What SO_PEERCRED is for #

SO_PEERCRED is useful for:

  • Logging. "Connection from PID 12345 (UID 1001) at time T". This is a friendly identification, not a security claim.
  • Display. A daemon that wants to show "currently connected user: bob" can use the projected UID to look up the name.
  • Coarse compatibility. Linux applications that already use SO_PEERCRED for their own non-security checks continue to work.

For security purposes, it is the wrong tool. The information is too coarse; the connection between projected UID and authoritative identity is too easy to misread.

SCM_CREDENTIALS #

SCM_CREDENTIALS is a control message used with sendmsg/recvmsg on Unix sockets (including datagram sockets where SO_PEERCRED doesn't apply). The sender attaches a struct ucred to a message; the receiver extracts it.

On Linux, the sender can attach any struct ucred they like (subject to capability checks if the values would be different from their actual credentials). Peios projects the same way:

  • The sender's projected UID/GID is attached to the message.
  • The receiver can extract it via the control-message API.

The same caveats as SO_PEERCRED apply: the projected values do not carry the full identity, do not survive non-trivial state changes, and should not be used for security decisions.

SCM_CREDENTIALS is the way to attach credential info to datagram messages or socketpair-style sockets, where there is no "connect time" to capture peer info at.

Why these are insufficient for security #

The fundamental issue: the projection is for compatibility, not authority. UID 1001 corresponds to a SID, and that SID is what KACS uses for access decisions — but the projection doesn't carry the SID. It carries the projected UID, which is a derived value. A service that grants access based on the projected UID is making a decision based on a derivation, not the source of truth.

In practice, this leads to two classes of issue:

Identity collisions. Two different principals can in principle project to the same UID. The SID-to-UID mapping is typically 1-to-1, but reasonable failure modes (a misconfigured directory, a colliding mapping during a migration) can produce collisions. KACS sees them as different; UID-based code sees them as the same.

State that doesn't project. A token's integrity level, PIP, privileges, restricted-SID status — none of these project to UID/GID. A service that does "if the UID is admin's UID, grant access" cannot tell that the calling process is running on a restricted token with admin's SID but no actual privileges. KACS would deny most accesses; the UID-only check would grant them.

For services that need to be secure, neither SO_PEERCRED nor SCM_CREDENTIALS is the right tool. The right tool is the KACS-aware peer-token mechanism.

The replacement: kacs_open_peer_token #

kacs_open_peer_token is the KACS-native equivalent. The call:

token_fd = kacs_open_peer_token(socket_fd)

Returns a token fd reflecting the peer's complete identity at connect time. The fd carries TOKEN_QUERY | TOKEN_IMPERSONATE access — enough to inspect the token's full state and to install it as an impersonation token.

What the token includes:

  • The peer's user SID, group SIDs (with attributes), restricted SIDs, logon SID.
  • Their integrity level, mandatory_policy, PIP type and trust.
  • Their privileges (present, enabled, used, removed states).
  • Their confinement state (sid, capabilities, exempt).
  • Their session reference (auth_id).
  • Their claims (user and device).
  • Their default DACL, owner index, primary group index.
  • Their projected UID/GID (for completeness — the same values SO_PEERCRED would have returned).

Everything. The full identity, atomic, captured at connect time. From the receiver's perspective, this token is what KACS would have access-decided against if the peer had made the request directly.

The service can:

  • Inspect the token (via KACS_IOC_QUERY) to make authorisation decisions on real identity, not projected UID.
  • Impersonate the peer (via KACS_IOC_IMPERSONATE or kacs_impersonate_peer) to perform operations on their behalf — with the just-in-time pattern from Peer tokens and capture.

This is the security-grade peer-identity API. For services that need real access control on connecting peers, this is the tool.

When to use which #

A handful of guidelines:

Want toUse
Log who connected for diagnostic purposesSO_PEERCRED or projected UID-style API
Display a "connected as user X" indicatorSame
Make an access decision based on peer identitykacs_open_peer_token then KACS-aware logic
Act on the peer's behalf (impersonate)kacs_impersonate_peer or kacs_open_peer_token + KACS_IOC_IMPERSONATE
Capture peer identity at connect for later usekacs_open_peer_token (store the fd)
Send a credential along a datagram messageSCM_CREDENTIALS for compat; for security, pass the token fd via SCM_RIGHTS

The pattern: compatibility-grade peer ID uses the Linux APIs; security-grade peer ID uses the KACS APIs. The distinction matters most for services that handle untrusted callers — a public-facing daemon should use kacs_open_peer_token; an internal diagnostic tool can use SO_PEERCRED.

What about TCP sockets #

Linux's SO_PEERCRED does not work on TCP sockets — the peer is potentially remote, and there's no kernel-side credential to query. The same is true on Peios: KACS does not capture peer tokens over TCP. The peer is whatever the network layer says it is, identified by IP / connection state, not by a token.

For services accepting TCP connections from authenticated peers, the authentication is at a different layer — TLS with mutual certificates, Kerberos via authd, application-layer protocols. The result of that authentication is what determines the connecting principal's identity; the kernel-level peer-credential APIs are not involved.

Once authentication has produced a token (typically by authd), the service can install that token as an impersonation token via the normal KACS_IOC_IMPERSONATE path. The token-installation API is uniform regardless of whether the token came from a Unix-socket peer or a TCP-authenticated session.

What goes wrong if you use the wrong tool #

A few concrete failure modes from using SO_PEERCRED for security:

  • Sandbox bypass. A user runs a sandboxed application that connects to a privileged daemon. The application's token has the user's SID (restricted, with privileges removed). The daemon does SO_PEERCRED, sees UID 1001, looks the user up, sees they're in the administrators group, grants admin access. The sandbox is bypassed — KACS would have refused based on the restricted token, but the daemon never asked KACS.
  • Impersonation confusion. A service that connects to another service while impersonating a client should appear (to the receiver) as the client. With SO_PEERCRED, the receiver sees the client's projected UID — correct in this case. But if the impersonation changed mid-session (the service reverted, then made another call), SO_PEERCRED still shows the original peer's UID. The receiver can be confused about who they're actually serving.
  • Identity not present in projection. A service that wants to make decisions based on the peer's integrity level cannot — there's no projection for it. A SO_PEERCRED-using service sees nothing about integrity; the call effectively ignores that axis of access control.

For each of these, kacs_open_peer_token would produce the correct result because the token carries the full identity. The migration path for an existing service is: replace SO_PEERCRED calls with kacs_open_peer_token, use the token to make decisions instead of the projected UID. The code surface changes; the semantic is more accurate.

Compatibility, not removal #

SO_PEERCRED and SCM_CREDENTIALS are not being removed. They continue to work with sensible semantics for compatibility with Linux software. The recommendation is to migrate security-sensitive code to KACS-aware APIs, not to deprecate the Linux APIs.

For most services this means: keep the Linux API for friendly identification; add KACS-aware logic for access decisions. Both can coexist on the same connection; the security paths use the KACS-aware data, the diagnostic paths use the Linux API.

Where to go next #

For how the projected UID/GID values are computed, read Credential projection.

For capturing and impersonating a peer's full token, read Peer tokens and capture.

For what a token carries that a struct ucred cannot, read Tokens.

Linux relics

Peios / Advanced Peios / Linux compatibility

A handful of Linux features are relics: they still exist and still work the way they do on Linux — so software depending on them keeps running — but they have been superseded and are not part of how anything is meant to be done on Peios. Peios does not document them in depth. For the mechanics of the underlying Linux feature, the authoritative source is third-party Linux documentation (the relevant man pages and the kernel's own docs). Where Peios has a native replacement for what the relic was used for, this page points to it — that, not the relic, is the path to take.

A feature belongs here only if it is genuinely superseded, has no role in Peios's future, and gives essentially no reason to reach for it. This is a short list by design, not a place to park anything inconvenient to document.

Process accounting (acct) #

acct() is BSD process accounting. Called with a filename it turns on system-wide accounting, appending a fixed binary record for every process as it terminates — resource usage, timing, exit status, and a few behaviour flags; called with no argument it turns accounting off. Enabling or disabling it requires SeTcbPrivilege (the privilege Linux's CAP_SYS_PACCT maps to). For the record format and per-field detail, see the Linux acct(2) and acct(5) documentation.

On Peios you almost certainly want eventd and KMES instead: process lifecycle is already an event stream there, keyed on each process's GUID and carrying its real identity, exit status, and resource usage — structured and queryable, where acct produces an opaque append-only file built around the Linux UID model. See the eventd and KMES material for the native way to account for what processes do.

Where to go next #

For the compatibility model that decides what survives and how, read Linux compatibility.

For the native event pipeline that replaces process accounting, read Events and transport.

For following that event stream from the command line, read The event stream.

Identity for POSIX programs

Peios / Advanced Peios / Linux compatibility

A Linux program calls getpwuid and has never heard of a token. Between that call and the authority sits one shared object:

/usr/lib/libnss_peios.so.2

It asks authd over /run/ident.sock and renders the answer as a struct passwd.

There is nothing to configure #

On other systems /etc/nsswitch.conf decides where identity comes from. On Peios it does not, for passwd, group, shadow or initgroups. glibc is patched so those four reach the authority and nothing else, whatever any file says.

That is not tidiness. A second search order the authority cannot see is a second answer to the question who is jack, and a program acting on one principal's behalf while its access is checked against another is a confused-deputy bug rather than a cosmetic inconsistency. The authority resolves a name across the principal sources it is configured to have, in a configured order, applying domain and numeric confinement — none of which nsswitch.conf can express, and all of which a line in it would bypass.

It also closes an extension point Peios does not want. Naming a module in nsswitch.conf injects a shared object into every address space on the system. That is the in-process-plugin pattern this design rejects everywhere else it appears. Adding a source of identity to a Peios machine means writing a principal source — a separate process the authority confines, which cannot mint.

hosts, services, networks and the rest are untouched. They are not identity, and coupling them to this would be a decision about DNS taken for reasons about principals.

There is no /etc/passwd #

No files entry, and nothing behind the authority to fall back to.

There is no root to hold there. uid 0 is where the SYSTEM token projects, not an account — nothing on the system looks for a principal called root, and peinit projects SYSTEM itself without resolving anything. A flat file of accounts would be a second identity store with none of the confinement the real one has, holding entries for principals that do not exist.

shadow and gshadow return nothing, always. A Peios verifier lives in its source's store and cannot be read out at all, so there is no entry to return and never will be.

PAM does not exist on Peios. There is no libpam, no /etc/pam.d, and no stack to configure. Authentication goes through PGSS Logon, where a client collects what it is asked for and an authority decides — and the module-stacking model PAM is built on is the same one the paragraph above rejects.

Before the authority, nothing resolves #

Anything running before authd — peinit, an initramfs — gets numbers rather than names.

That is the correct answer rather than a gap. Identity comes from the authority; until it exists there is none to have, and a file that answered anyway would be answering for principals nobody had vouched for.

Note

A program that treats an unresolved uid as a fatal error will fail early in boot. Displaying the number is the expected behaviour.

One call, one round trip #

The module holds itself to a rule: each libc call costs exactly one request.

  • getpwuid asks for the six fields a passwd record needs, together.
  • getgrnam asks for the group's members with their names already resolved, so filling gr_mem costs nothing further. A reply of bare identifiers would have turned one call into one per member.
  • initgroups asks about the principal, which is the direction sources actually store memberships — it never walks a group's membership to get there.

It opens a connection per call rather than holding one. A shared object cannot see its process fork, and a connection inherited by a child that then interleaves requests on it with its parent is a well-known way for a name resolver to hand back somebody else's answer. Connecting to a Unix socket is cheap; the caching that would be cheaper belongs in the authority, where every process shares it and something can tell it when it goes stale.

What the return values mean #

Resultglibc statusEffect
The principal existsSUCCESSThe record is returned.
No such principalNOTFOUNDThe caller sees no such user.
A source did not answerTRYAGAIN (EAGAIN)The caller retries. Not an absence.
authd is not reachableUNAVAILNothing is behind it; the lookup fails.

The third row is the one that matters. A source that could have answered and did not is not the same as an account that does not exist, and reporting it as one would let an outage be remembered as a fact — the account comes back when the source does, but a cached "no such user" would not.

What POSIX cannot express #

The rendering is one-directional and lossy, and each loss is a property of struct passwd rather than of anything underneath it.

No claims. A principal's claims feed conditional ACEs and have no field in a passwd record. Peios-native tools see them; getpwnam does not.

No domain. A uid_t is a number. Which source and which domain it came from is recoverable — the ranges are laid out so it is — but nothing in the record carries it.

Empty member lists. gr_mem for Everyone is empty, because nothing records who is in Everyone; the authority adds it to every token it mints. Inventing a list of this machine's principals would be a wrong answer rather than a partial one. See resolving names for the three kinds of membership and which of them can be listed.

Unnumbered groups are skipped. Interactive and its siblings have no POSIX group id, because membership in them is a property of a session rather than of an account. They are omitted from a supplementary group list rather than rendered as nobody, which would grant whatever nobody can reach.

getent and the whole list #

getent passwd works, and pages through every source in turn.

A source is never required to enumerate — a directory able to answer any single question may be quite unable to answer all of them — so a listing can legitimately be partial. The authority records which sources did not contribute; a Peios-native tool can show that, though getent itself has nowhere to put it.

Tip

A short getent passwd on a machine with a directory source is worth checking against authd's log before concluding an account is missing.

Peios Learn — documentation for the Peios project.

Built with Trail.