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

Process Integrity Protection

Single-page view · as markdown

Process integrity protection

Peios / Peios Security Fundamentals / Process Integrity Protection

Process integrity protection — PIP — is the access-control layer that gates one process's ability to operate on another. Where the DACL and tokens decide "is this principal allowed?", PIP decides "is the calling binary trusted enough?". A PIP-protected process — peinit, authd, the kernel's own helpers — cannot be debugged, signalled, suspended, or read from by code running at lower trust. Even root cannot.

The protection is rooted in the binary's signature. At exec, the kernel verifies the new program against its public-key catalog and assigns the resulting process a two-dimensional trust label — pip_type and pip_trust — that records what kind of trust the binary established and at what level. From then on, every cross-process operation against this process compares the caller's label to this one.

This is not the same axis as identity. Two processes running as the same user can have different PIP labels because they are running different binaries. A user-mode process and a TCB daemon running as SYSTEM are at different PIP levels; the user-mode process cannot interfere with the daemon even though both run as the same principal. The barrier is not who, it is what.

Where PIP lives #

Each process has a Process Security Block — the PSB — attached by the kernel. The PSB carries:

FieldMeaning
pip_typeThe PIP type. One of None, Protected, Isolated.
pip_trustThe PIP trust level. A numeric tier within the type.
security_descriptorThe process SD — see The process security descriptor.
Mitigation flagsThe process's enabled security mitigations, including NO_CHILD (whether the process may fork) — see Process mitigations.

The PSB is not the token. The token says who the process is acting as; the PSB says what kind of process it is. The two have different lifecycles, different fields, and are read by different parts of the access pipeline. A thread impersonating a user changes its effective token but not its process's PSB. The PSB is fixed once the binary execs.

The 2D trust model #

PIP's trust is encoded as two numbers, deliberately not collapsed into a single ordering:

AxisRangeWhat it means
pip_type0 (None), 512 (Protected), 1024 (Isolated)The kind of trust. None = unprotected; Protected = the standard PIP-protected process; Isolated = reserved for future use.
pip_trustnumeric (0, 1024, 1536, 2048, 4096, 8192 in v0.20)The tier of trust within a type. Higher numbers are more trusted within the same type.

The 2D model exists because trust is not a single line. A signed application from a third-party developer is trusted for what it is — its publisher attested to it — but is not at the same trust as a Peios TCB binary that has been signed by the OS itself. They have different types of trust, and within each type, there can be tiers.

In v0.20 the catalog is small — the full list of S-1-19-T-L label SIDs is in Well-known SIDs. The shape of the ladder, by example:

SIDtypetrustWhat it represents
S-1-19-0-0None (0)0Unprotected. Default for unsigned processes.
S-1-19-512-2048Protected (512)2048Peios-distributed applications.
S-1-19-512-8192Protected (512)8192Peios TCB (peinit, authd, loregd, lpsd, eventd).

The Isolated type (1024) exists for a future where some processes need to be protected from even the rest of the TCB. In v0.20 it is documented for forward compatibility; no binary will receive this type.

Most processes on a running system have pip_type = None. PIP protection is opt-in via signing — most user-mode applications run unsigned and unprotected. The PIP-protected processes are the kernel's specifically-signed daemons.

How PIP gets assigned #

The kernel sets pip_type and pip_trust at exec time and never changes them afterwards:

  1. The kernel reads the binary about to be exec'd.
  2. It looks for a signature — an ELF .peios.sig section or a security.peios.sig xattr.
  3. It verifies the signature against its compiled-in public key catalog.
  4. On a valid signature, the PIP fields are set to whatever level the catalog says the signing key represents.
  5. On no signature, invalid signature, or any verification failure, pip_type is set to None (0) and pip_trust to 0. The exec still succeeds — bad signatures do not block execution, they only fail to grant PIP protection.

Once exec completes, the fields are immutable for the lifetime of that process. fork() inherits the parent's PIP fields. exec() recomputes them from the new binary.

The signing and verification mechanics — what the signature format is, how the catalog works, why bad signatures do not block exec — are in Binary signing.

What PIP gates #

PIP fires on every cross-process operation that requires the caller to inspect or affect another process:

  • Signals (kill, signal delivery).
  • ptrace and its kin (memory read/write, attach, single-step).
  • pidfd_open, pidfd_getfd, pidfd_send_signal.
  • Reading or writing /proc/<pid>/* for processes other than oneself.
  • Opening another process's tokens (kacs_open_process_token, kacs_open_thread_token).
  • Setting process attributes (priority, affinity, rlimits).
  • Profiling another process (perf_event_open).

In every case, the kernel runs the two-check rule: the calling process must pass both an SD check (the target's process SD must grant the requested right to the caller's token) and a PIP dominance check (the caller's PIP label must dominate the target's). Both must succeed. Neither alone is enough.

The full mechanism is in The two-check rule. What matters for the overview: PIP does not replace identity-based access control, it sits alongside it. A PIP-protected process can still be administered by an SD that grants the rights; the SD just is not the whole story.

What PIP does not do #

A few clarifications that come up:

  • PIP is not encryption. A PIP-protected process's memory is in plaintext. The kernel does not encrypt it or seal it; it just refuses ordinary access channels to it. A compromised kernel or a hardware-DMA attack bypasses PIP entirely.
  • PIP is not capability isolation. Two PIP-protected processes at the same level can interfere with each other freely. The level is the boundary, not the identity.
  • PIP is not a sandbox. A PIP-protected process is not contained — it is protected from containment-bypassing code. PIP protects high-trust processes from low-trust ones, not the other way around.
  • PIP is not user-configurable. There is no API to upgrade a process's PIP level at runtime. The signature catalog is in the kernel; only re-execing a different binary changes the level.
  • PIP does not require the binary to be running as a particular user. A user-mode app run by an administrator and signed at TCB level would still be a TCB-level process from PIP's perspective. The signature is what matters, not who started it.

The cleanest mental model: PIP is "what kind of program is this, and who is allowed to mess with it?". The token is "who is this program acting as, and what can they reach?". Each answers a different question. Both run.

Where to start #

If you want the process SD specifically — the SD that lives on the PSB and gates operations on the process — read The process security descriptor.

If you want the dominance comparison and the two-check rule — the rule that every cross-process operation runs both an SD check and a PIP dominance check — read The two-check rule.

If you want the edge cases — how PIP interacts with impersonation, how it differs from MIC, why peinit ends up being the lifecycle manager for PIP-protected processes, and what the model does not cover — read PIP in practice.

The process security descriptor

Peios / Peios Security Fundamentals / Process Integrity Protection

A process is an object the kernel protects, like any other. It carries a security descriptor — the process SD — that defines who can do what to this process. Where the token's SD controls who can do what to the token (read, duplicate, impersonate), the process SD controls who can act on the running process itself: signal it, debug it, inspect its memory, change its priority, open its tokens.

The process SD sits on the PSB, not the token. The token's SD is a separate field, governing token operations. The two are distinct objects with distinct policies. A change to one does not affect the other.

This page covers what the process SD contains, the access rights it uses, how it is created, and how it is modified. The dominance pairing — how the process SD plus PIP forms the two-check rule — is in The two-check rule.

What the process SD contains #

The process SD has the same shape as any other SD — owner, primary group, DACL, optional SACL. The fields mean the same things they mean elsewhere (see Security descriptors). What differs is the access rights the DACL grants.

The DACL on a process SD uses the process access rights — a set of 16-bit object-specific rights tailored to what one might do to a process. The full catalog with bit values is in Access mask bits; in summary:

RightWhat it gates
PROCESS_TERMINATESend terminating signals (SIGKILL, SIGTERM, SIGABRT, etc.).
PROCESS_SIGNALSend non-terminating informational signals (SIGCHLD, SIGURG, SIGWINCH, etc.).
PROCESS_VM_READRead the process's memory. Required by ptrace(PTRACE_PEEK*), process_vm_readv, /proc/<pid>/mem reads.
PROCESS_VM_WRITEWrite the process's memory. Required by ptrace(PTRACE_POKE*, PTRACE_ATTACH), process_vm_writev, /proc/<pid>/mem writes.
PROCESS_DUP_HANDLEDuplicate file descriptors out of the process via pidfd_getfd.
PROCESS_SET_INFORMATIONChange process attributes — scheduling priority, CPU affinity, rlimits, certain /proc/<pid>/* writes.
PROCESS_QUERY_INFORMATIONRead detailed process information — token, full /proc/<pid>/* reads (cmdline, status, io, limits, sched, mounts).
PROCESS_SUSPEND_RESUMESend stop/continue signals (SIGSTOP, SIGCONT).
PROCESS_QUERY_LIMITEDRead limited process information (PID, image name, basic state). Required by pidfd_open.

Plus the standard rights — DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, SYNCHRONIZE — and the special rights ACCESS_SYSTEM_SECURITY and MAXIMUM_ALLOWED. These mean the same things on a process as on any other object.

PROCESS_TERMINATE and PROCESS_SIGNAL are split because terminating signals are operationally different from informational ones. A monitoring tool might be allowed to send SIGCHLD-style signals without being allowed to kill the process. This is the kind of decomposition the process rights catalog makes possible.

Generic mapping #

The standard generic rights (GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, GENERIC_ALL) map to combinations of process-specific rights via the process GenericMapping table, cataloged in Access mask bits.

A DACL ACE that grants GENERIC_READ to some principal effectively grants them the right to read the process's memory, query its detailed information, and read the SD itself. A DACL granting GENERIC_EXECUTE lets them terminate or suspend the process. These are the abstractions tools use when they say "grant read access to this process" without enumerating every specific right.

How the process SD is created #

A new process's SD is set up at process creation. The path:

  1. At fork, the child inherits a copy of the parent's process SD. The child's process is a different object from the parent's, but the SD is structurally the same.
  2. At exec, if the new binary's signing changes the PIP fields significantly (in particular, if the new PIP type is non-zero where the old was zero, or if a KACS_IOC_INSTALL of a different user-identity token happens between fork and exec), the kernel may regenerate the process SD from a default template based on the new token's identity. The intent is that a process running a TCB binary should have a TCB-flavoured process SD, not whatever its parent had.
  3. The default template, when used, grants the process's user identity PROCESS_QUERY_LIMITED and PROCESS_QUERY_INFORMATION, grants BUILTIN\Administrators and SYSTEM the right to inspect and signal, and denies everything else to ordinary callers.

For typical user processes, the SD that ends up on the PSB is the inherited copy of the parent's. For TCB processes — the daemons signed at TCB trust level — the SD is regenerated to reflect the role of the process.

How the process SD is modified #

The process SD on a running process can be updated via kacs_set_sd on the PSB, just like setting an SD on a file. The caller needs WRITE_DAC on the target process (for the DACL) and the appropriate rights for owner, group, and SACL changes. The standard SD modification rules from Security descriptors apply.

A common use case: a service whose SD needs to be tightened after startup. The service launches with a default SD that lets the supervisor inspect and signal it; once the service is in steady state, it tightens its own SD to remove some of those rights, restricting which external callers can affect it.

Another use case: a process voluntarily allowing another to debug it. Adding an ACE granting PROCESS_VM_READ | PROCESS_VM_WRITE to a specific helper SID lets that helper attach a debugger without administrator intervention.

The process SD does not affect PIP. Changing the SD does not change the PIP fields. The two are independent — the SD-on-PSB gates "who is allowed" while PIP gates "what trust level is allowed". A debugger added to the SD still needs to PIP-dominate the target to actually debug it.

What the process SD does not do #

A few clarifications:

  • The process SD does not gate the process's own access to objects. It controls operations targeting the process. What the process itself can do is governed by its token, not by its own SD. A process can be PROCESS_TERMINATE-denyed by its own SD (other processes can't kill it) while still being free to do whatever its token allows.
  • The process SD does not replace PIP. A non-PIP-protected process can have a restrictive SD; an inspector granted PROCESS_VM_READ by the SD can still read its memory if PIP permits. A PIP-protected process needs both the SD grant AND PIP dominance — see The two-check rule.
  • The process SD does not control file access. Each open file has its own SD; the process's SD is unrelated to what files the process can open.
  • The process SD does not control token operations. The token has its own SD; opening or duplicating tokens goes through that, not the process SD (although the token operations also check process-level rights as a wrapper — kacs_open_process_token requires PROCESS_QUERY_INFORMATION on the process AND the appropriate right on the token).

The cleanest mental model: the process SD answers "may I (the caller) signal, debug, or inspect that process?". Everything else about the process's behaviour, identity, and authority lives in other SDs or in the token.

Where to go next #

For how the process SD pairs with PIP dominance on every cross-process operation, read The two-check rule.

For the general SD model these fields come from — owner, DACL, SACL, and the modification rules — read Security descriptors.

The two-check rule

Peios / Peios Security Fundamentals / Process Integrity Protection

When one process wants to act on another — to signal it, debug it, read its memory, change its priority, or open its tokens — the kernel runs two checks. The SD check asks "is the caller allowed by the target's process SD?". The PIP dominance check asks "is the caller's PIP label at least the target's?". The operation proceeds only if both checks pass. Either one failing is enough to deny.

The two-check rule is the rule that gives PIP its bite. Without it, an administrator's token would let any administrator-run code reach into a TCB daemon and read its memory; with it, the calling binary's trust level becomes a separate gate that no identity, no group, no privilege can bypass.

This page covers the rule itself: what each check looks at, how the dominance comparison works, what privileges can and cannot bypass, and how specific kernel operations map onto the rule.

The two checks, side by side #

flowchart LR
    A["Cross-process operation"] --> B["SD check: process SD vs caller's token"]
    A --> C["PIP check: caller's PSB vs target's PSB"]
    B -->|grants the required right| D["AND"]
    B -->|denies| F["Operation denied"]
    C -->|caller dominates target| D
    C -->|caller does not dominate| F
    D --> E["Operation allowed"]

Both checks run at the entry point of the operation. They are not sequenced — there is no "first the SD check, then PIP". The kernel reads both inputs and computes both results; the operation proceeds only on both passing.

CheckReadsComparesDecides
SD checkTarget's process SD; caller's effective tokenStandard AccessCheck pipeline against the SD's DACLWhich process rights the caller has on the target.
PIP dominanceCaller's PSB (pip_type, pip_trust); target's PSBAll-or-nothing comparison: caller's type ≥ target's type AND caller's trust ≥ target's trustWhether the caller is trusted enough to act on the target.

Each check is independent. A caller can pass one and fail the other. The operation result is pass_SD AND pass_PIP.

The dominance comparison #

PIP dominance is two-axis, conjunctive. The caller's PSB carries pip_type and pip_trust. The target's PSB carries the same fields. The caller dominates the target if and only if:

caller.pip_type   >= target.pip_type   AND
caller.pip_trust  >= target.pip_trust

Both inequalities must hold. Both holding is dominance. Either failing is non-dominance.

A few non-dominance cases worth pinning:

  • Same type, lower trust. A Protected/1024 caller does not dominate a Protected/8192 target. Same trust kind, but the target is more trusted.
  • Higher trust, lower type. A None/8192 caller (which would be weird in v0.20 but legal in principle) does not dominate a Protected/0 target. The trust level is higher numerically, but the type is lower; both have to dominate independently.
  • Different types, neither higher. A Protected/4096 caller does not dominate an Isolated/0 target. Protected (512) is less than Isolated (1024), so the type comparison fails. Even though the trust number is higher, the type fails the AND.

The comparison is not ordered. There is no "single PIP score" that says "you are more trusted than that other process". A caller can be more trusted on one axis and less on another, and the dominance check declines to compare further — without dominance on both axes, the operation is denied.

This two-axis structure is what lets the model distinguish "Authenticode-signed third-party app" from "Antimalware tool" — both are Protected type, but one's purpose is different from the other's, and they need not be ordered. The trust tier reflects the role; the type reflects the kind of authority.

The non-dominance default #

A caller with pip_type = None (the unsigned, default state) dominates only targets that also have pip_type = None. They cannot reach any Protected or Isolated target. Most user-mode processes are in this state — they can interfere with each other freely, but not with the TCB daemons.

A caller with pip_type = Protected dominates other Protected processes at the same or lower trust tier, and dominates None processes entirely. But not Isolated.

A caller with pip_type = Isolated (none in v0.20) dominates everything — Protected and None at any trust level, plus other Isolated processes at the same or lower trust.

The most common protective barrier is the None-to-Protected boundary. User-mode applications (pip_type = None) cannot signal, debug, inspect, or modify the running TCB processes (pip_type = Protected). This is the practical security boundary PIP creates.

What SeDebugPrivilege bypasses #

SeDebugPrivilege is the closest thing to an override for the SD check, but it does not override PIP.

The privilege, when present and enabled on the caller's token:

  • Bypasses the SD check for cross-process operations. A caller holding the privilege does not need the target's SD to grant them the required process rights; the SD check is treated as passing regardless of what the DACL says.
  • Does not bypass the PIP dominance check. A caller holding the privilege still must dominate the target's PIP. If they do not, the operation fails.

This is the most important rule about SeDebugPrivilege. The privilege gives the holder the ability to debug arbitrary user processes, but it does not give them the ability to reach into the TCB. A debugger with SeDebugPrivilege can attach to any user-mode process they can find; it cannot attach to a TCB daemon. The privilege handles identity-based gating; PIP handles trust-based gating, and the privilege has no power over the trust axis.

Tools that need to debug TCB processes need a different mechanism — typically running as a TCB binary themselves (i.e., a debugger signed at TCB level). There is no privilege that opens this door. The signature is the only path.

What other privileges do not bypass #

A few other privileges are worth noting:

  • SeTcbPrivilege does not bypass PIP either. The privilege grants a lot — token creation, mount-policy administration, central-policy distribution — but it operates within the PIP boundary. A TCB caller (holding SeTcbPrivilege) is typically also a TCB-trust-level process anyway, so the dominance is satisfied by virtue of the binary, not the privilege.
  • SeBackupPrivilege and SeRestorePrivilege do not bypass PIP. A backup tool granted access to read another process's memory via the SD check still needs to dominate PIP.
  • SeImpersonatePrivilege does not bypass PIP. Impersonation changes the effective token; it does not change the PSB. A high-trust client's impersonation token does not promote the impersonating server's PIP level.

The pattern: no privilege in the catalog bypasses PIP dominance. PIP is the trust ceiling that operates above the privilege model. It is the answer to "what if a malicious administrator gets a privileged token?" — the answer is "they still cannot reach the TCB, because the TCB is at a higher PIP level than any binary they could run".

The operations the rule applies to #

The two-check rule fires on every cross-process operation. The full list of kernel surfaces it covers:

OperationProcess right neededNotes
kill, signal deliveryPROCESS_TERMINATE, PROCESS_SUSPEND_RESUME, or PROCESS_SIGNAL (by signal class)PIP dominance also required. Classified by default disposition — see Signals in detail for the full rules, the self-exemption, and the kernel-originated bypass.
kill -STOP, kill -CONTPROCESS_SUSPEND_RESUMEPIP dominance required.
ptrace(PTRACE_ATTACH), ptrace(PTRACE_POKE*)PROCESS_VM_WRITEPIP dominance required. SeDebug bypasses the SD; not PIP.
ptrace(PTRACE_PEEK*)PROCESS_VM_READSame.
PTRACE_TRACEMEPROCESS_VM_WRITE on caller's SD; PIP dominance of the nominated tracer over the calling processReverse-flavoured of the others.
pidfd_openPROCESS_QUERY_LIMITEDPIP dominance required.
pidfd_getfdPROCESS_DUP_HANDLEPIP dominance required.
process_vm_readv, process_vm_writevPROCESS_VM_READ, PROCESS_VM_WRITERoute through the ptrace check.
/proc/<pid>/mem read/writePROCESS_VM_READ / PROCESS_VM_WRITESame.
/proc/<pid>/ basic reads (stat, comm, wchan)PROCESS_QUERY_LIMITEDPIP dominance required.
/proc/<pid>/ detailed reads (cmdline, status, io, limits, sched, mounts)PROCESS_QUERY_INFORMATIONPIP dominance required.
/proc/<pid>/ writes (sched, oom_adj, coredump_filter)PROCESS_SET_INFORMATIONPIP dominance required.
sched_setaffinity (cross-process)PROCESS_SET_INFORMATIONPIP dominance + SeIncreaseBasePriorityPrivilege.
setpgid (cross-process)PROCESS_SET_INFORMATIONPIP dominance required.
getpgid, getsid (cross-process)PROCESS_QUERY_LIMITEDPIP dominance required.
perf_event_open (target-specific)PROCESS_QUERY_INFORMATIONPIP dominance required + SeProfileSingleProcessPrivilege. SeDebug bypasses SD only.
capget(pid) (cross-process)PROCESS_QUERY_INFORMATIONPIP dominance required.
kacs_open_process_tokenPROCESS_QUERY_INFORMATION on the process AND the rights on the tokenPIP dominance required.
kacs_open_thread_tokenSamePIP dominance required.

In every case, two checks: process SD grants the right, and caller's PIP dominates target's. Failing either denies.

Same-process operations #

The two-check rule applies only to cross-process operations. When a thread acts on its own process — opening its own token, reading its own memory, signalling itself — neither check fires. A process can always operate on itself.

This is what makes the model usable. PIP is a boundary between processes, not a constraint on what a process can do internally. A TCB daemon can read its own memory, signal itself, modify its own token state without going through the two-check rule.

What happens on failure #

When the two-check rule denies an operation, the failure mode depends on the operation:

  • Signal delivery returns -EPERM.
  • ptrace returns -EPERM (or -ESRCH in some no-trust-revealing paths).
  • pidfd_open, pidfd_getfd return -EACCES.
  • /proc//* reads return -EACCES or, in some cases, hide the directory entry entirely (the kernel does not reveal information about processes a caller cannot reach).
  • kacs_open_process_token returns -EACCES.

The kernel deliberately does not always distinguish "you do not have the right" from "this process does not exist" — for some sensitive operations, returning the same error in both cases prevents a low-trust caller from learning about the presence of high-trust processes.

Signals in detail #

Signal delivery is the two-check operation with the most edge cases, so the rules deserve spelling out.

Which right a signal needs is decided by its default disposition. Signals whose default action terminates the target — SIGTERM and SIGKILL, but also SIGHUP, SIGINT, SIGUSR1/2, SIGPIPE, and every realtime signal — require PROCESS_TERMINATE. The job-control set (SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT) requires PROCESS_SUSPEND_RESUME. Signals ignored by default (SIGCHLD, SIGURG, SIGWINCH) require PROCESS_SIGNAL. The kill(pid, 0) existence probe delivers nothing and requires PROCESS_QUERY_LIMITED — it is a query, and it is treated as one.

Self-signaling is exempt, structurally. A thread signaling its own process — raise(), abort(), pthread_kill() to a sibling thread — never runs either check. This does not rely on the default process SD granting the process access to itself: it holds even for restricted or confined tokens that would fail an access check against their own SD. A sandboxed process can always abort itself.

Kernel-originated signals bypass the checks entirely — there is no sending process to check. This covers hardware faults (SIGSEGV, SIGBUS), kernel notifications (SIGCHLD on child exit, SIGPIPE on broken pipe), and — the case worth pausing on — terminal-generated job control. When you press Ctrl-C, the tty driver signals the foreground process group; no permission check runs, and the interrupt reaches the foreground processes even if they are more privileged or more PIP-trusted than you. This is deliberate: authorization for keyboard signals is possession of the controlling terminal (gated by the terminal's file SD when it was opened), and a privileged process that attaches to your terminal has chosen to take input from you. Note the limit: this path only carries the terminal's own signals — nobody can use it to deliver an arbitrary kill().

Group kills are checked per target. kill(-pgid, ...), kill(0, ...), and kill(-1, ...) evaluate each candidate process independently, deliver to the subset the caller may signal, and succeed if at least one delivery happened. A mixed-privilege process group (an elevated member in your pipeline) gets partial delivery — the same behavior Linux exhibits for mixed-uid groups.

There is no same-session SIGCONT exception. POSIX carves out "any process may SIGCONT members of its own session"; Peios does not implement it — SIGCONT needs PROCESS_SUSPEND_RESUME like the rest of the job-control set. In practice the default process SD's user-SID entry covers resuming your own stopped jobs; the divergence only surfaces when resuming a same-session process whose identity no longer matches yours.

si_pid and si_uid are informational. The sender identity carried in siginfo_t is the sender's projected UID captured at send time, plus a PID that may have been recycled by the time you read it. Like SO_PEERCRED, these are for logging and display, never for authorization.

Why both checks exist #

It is reasonable to ask: why not collapse the two checks into one? Either the SD knows about the trust level, or PIP knows about the SD; pick one.

The answer is that the two checks model different things and have different administrators:

  • The SD says "who, by identity, can act on this process". An object owner adjusts this DACL like any other. The rights are operationally meaningful (signal vs read vs debug) and are the natural unit for fine-grained access control.
  • PIP says "what kind of binary can act on this process". The kernel sets this based on the binary's signature; no administrator can adjust it directly.

If you only had the SD, every binary that ran with the right token could reach any process. Privilege isolation would not exist; an administrator's debugger could attach to the TCB. Conversely, if you only had PIP, fine-grained per-process access control would be impossible; the TCB would be all-or-nothing.

The two together give you both: identity-based control with PIP as a hard ceiling above it. The combination is the security model.

Where to go next #

For the practical consequences of the rule — the impersonation asymmetry, peinit as lifecycle manager, and the v0.20 limitations — read PIP in practice.

For where PIP's checks sit in the wider AccessCheck pipeline, read Access decisions.

PIP in practice

Peios / Peios Security Fundamentals / Process Integrity Protection

The PIP model is small once you have the dominance rule and the two-check rule in hand. But the practical consequences of those two rules ripple outward in ways that are not obvious from the rules themselves. PIP's asymmetric relationship with impersonation; the operational pattern that ends up forcing peinit to be the lifecycle manager for every PIP-protected process; the contrast with MIC; the threat-model ceiling above which PIP simply does not protect; and the v0.20 limitations of the implementation.

This page covers each.

PIP and impersonation #

PIP reads the PSB, not the effective token. Impersonation changes the effective token; it does not change the PSB. The consequence: impersonating a high-trust identity does not raise a process's PIP level.

A worked example. A user-mode application — pip_type = None — accepts a connection from a TCB daemon. The TCB daemon connected at Delegation level. The application calls kacs_impersonate_peer and now has the TCB daemon's identity installed as its effective token. The token is at SYSTEM integrity, with TCB-flavoured group memberships, with many privileges enabled.

What can the application now do?

  • Access checks that use the effective token (file access, registry access, regular DACL evaluation) succeed with the TCB daemon's authority. The application can open TCB-only files.
  • Access checks that use the PSB (PIP dominance for cross-process operations) still see the application's None-level PSB. The application cannot signal or debug TCB daemons — its PIP did not change.

The asymmetry is deliberate. PIP is a property of the binary running, not of who the binary is acting as. Allowing impersonation to raise PIP would let any service that ever accepts a TCB-level client effectively become TCB-level for the duration. The model is built on the idea that the binary is what is trusted, and a binary cannot lend its trust through impersonation.

The same asymmetry applies in reverse: if a TCB daemon impersonates a user-mode client (rare; usually it just acts as them at the file-access level), the TCB daemon's PSB is still TCB-level. The impersonation does not lower its PIP.

MIC vs PIP #

PIP and MIC look superficially similar — both compare a numeric level on the caller against a level on the object, both block when the caller is below. The two diverge in several ways that matter.

MICPIP
What it labelsTokens (integrity_level)Processes (pip_type, pip_trust) on the PSB
What it reads at access timeEffective token (impersonation-visible)PSB (impersonation-invisible)
Axes1 (integrity level)2 (type and trust)
DefaultMedium with NO_WRITE_UP for unlabelled objectsNone — objects are PIP-unrestricted unless they opt in via a label ACE
Bypassed by privileges?Privilege-granted bits surviveNo — privilege-granted bits are revoked for non-dominant callers
Default policyBlock writes from belowOnly the rights in the trust-label ACE's mask are permitted; everything else is denied

The two layers complement each other. MIC is the integrity axis for token-driven access (where impersonation matters); PIP is the trust axis for process-driven access (where the binary matters). An access can be blocked by either, or allowed by both.

The most consequential difference is the privilege handling. MIC respects privilege grants; PIP does not. A backup tool with SeBackup granted to it can read across MIC boundaries (read a higher-integrity file) because privileges preserve bits through MIC. The same backup tool cannot read across PIP boundaries — SeBackup does not grant the read on a PIP-protected object if the caller does not dominate. The privilege grant is stripped at the PIP check.

This is the security model's load-bearing assertion: privileges are not enough. A privileged but untrusted binary cannot bypass PIP-protected objects.

peinit as the lifecycle manager #

A PIP-protected process can only be signalled by a process that PIP-dominates it. For the TCB daemons — Protected/8192 — that means the signalling process needs at least Protected/8192. Which means it needs to be a TCB-signed binary itself.

In practice, the only process that fits this description and exists for the express purpose of managing other processes is peinit. peinit is signed at TCB level, runs at PIP Protected/8192, and is the system's init-equivalent.

The consequence: every PIP-protected process's lifecycle — start, restart, shutdown — flows through peinit, because no other binary can send the relevant signals. An ordinary administrator cannot use systemctl stop authd (or its Peios equivalent) directly; they ask peinit to do it. Even SIGTERM from a shell would be denied if the shell is not PIP-dominating.

This is the lifecycle manager pattern. It is not enforced by a specific kernel mechanism (peinit is not "marked" as the lifecycle manager); it falls out naturally from the PIP rule. peinit happens to be the only thing that can talk to TCB processes.

Practical implications:

  • Tooling that interacts with the TCB sends commands to peinit, not directly to the TCB processes.
  • Restart loops are implemented in peinit. If a TCB daemon crashes, peinit is the entity that decides whether and how to restart it — no external supervisor can.
  • Service-management tools for non-TCB services can be more direct, because non-TCB processes are not PIP-protected and ordinary signal-sending works.

Privileges and PIP, in detail #

The relationship between privileges and PIP comes up enough to be worth pinning. The general rule: privileges do not bypass PIP. The specific cases:

  • SeDebugPrivilege: bypasses the SD check on cross-process operations. Does not bypass PIP. A privileged debugger can attach to any user-mode process; it cannot attach to TCB processes.
  • SeBackupPrivilege / SeRestorePrivilege: grants read/write bits through AccessCheck. Bits granted by these privileges are subject to PIP — a non-dominant caller has them stripped during the PIP check for PIP-protected objects. (The grant happens at step 4 of AccessCheck; the PIP strip happens at step 5.)
  • SeImpersonatePrivilege: lets a service impersonate clients. Does not affect PIP; the PSB stays the same. The impersonated token may carry MIC at a higher level (capped by the integrity ceiling), but PIP does not move.
  • SeTakeOwnershipPrivilege: grants WRITE_OWNER regardless of DACL. Subject to PIP — a non-dominant caller cannot take ownership of a PIP-protected object even with the privilege.
  • SeTcbPrivilege: gates TCB operations (token creation, mount-policy administration). Held only by TCB processes anyway, which are PIP-dominant by virtue of their signing. The privilege does not extend PIP authority.

In short: every privilege the catalog defines operates within PIP, not above it. There is no privilege that bypasses PIP. The trust ceiling is absolute from the privilege model's perspective.

The threat-model ceiling #

PIP's protection is bounded by what the kernel can enforce. Specifically:

  • A compromised kernel voids PIP. If an attacker has code running in the kernel (e.g., via a malicious or buggy kernel module), they can read PSBs directly, modify memory directly, and bypass every PIP check. The defence against this is CONFIG_MODULE_SIG_FORCE=y — only signed kernel modules are accepted — but that defence is itself a precondition for PIP being meaningful.
  • Hardware DMA bypasses PIP. A device that can perform DMA can read or write any physical memory. The defence is IOMMU configuration, which is a kernel concern, not a PIP concern.
  • /dev/mem access bypasses PIP if writable. Even reading /dev/mem gives access to physical memory. The defence is CONFIG_STRICT_DEVMEM=y, which restricts /dev/mem to I/O regions only.
  • PIP does not provide cryptographic memory isolation. Two PIP-protected processes share the same physical memory and the same kernel. PIP is logical, not cryptographic; a kernel compromise sees through it.

These are not bugs in PIP — they are the kernel's responsibility, sitting above the PIP layer. PIP is the right defence for "a buggy user-mode application that an attacker is running"; it is not the right defence for "an attacker who has kernel code execution".

For a deployment where the threat model includes kernel-code attackers, PIP is insufficient on its own and must be paired with hypervisor-style isolation. That is a Peios-v2 concern, not a PIP concern.

What v0.20 does not do #

A handful of PIP limitations specific to v0.20 are worth knowing:

  • No per-binary revocation. A signed binary later found malicious cannot be invalidated short of removing it from the filesystem or replacing the kernel's signing key catalog. There is no hash-based revocation list.
  • The Isolated PIP type is reserved. pip_type = 1024 is documented and present in the model but no v0.20 signing key targets it. No binary will have this type until a future version defines its use.
  • Scripts are not signed. A script's PIP comes from its interpreter, not the script itself. A signed TCB interpreter running an untrusted script runs the untrusted code at TCB level. This is why interpreters are not signed at TCB unless the script content is itself trusted.
  • Only one signing key in v0.20 (the TCB key). Lower-trust tiers (App, Authenticode) are reserved in the catalog but no key exists for them in v0.20. All signed binaries on a v0.20 system are TCB binaries. App-level and Authenticode-level signing is future work.
  • PIP visibility in /proc. PIDs and directory names remain visible in /proc getdents() listings regardless of PIP — the kernel reveals that processes exist, even when their data cannot be read. Only file access within /proc/<pid>/ is gated. A future version may filter directory listings as well.
  • Coredumps are disabled for PIP-protected processes. A PIP-protected process's coredump is a potential secret leak; the kernel sets dumpability to false at exec and refuses to re-enable it via prctl(PR_SET_DUMPABLE, 1) while pip_type != None. The defence is mandatory.

These are limitations of the implementation, not the model. The model accommodates revocation, additional types, signed scripts, and richer visibility filtering; they just aren't there yet.

When PIP is the right tool #

A few patterns where PIP is the answer:

  • Protecting kernel-adjacent daemons. authd, loregd, eventd, and other TCB components must not be reachable by ordinary user code, even by administrators. PIP is the layer that enforces this. Their signing at TCB level produces processes that no non-TCB caller can interfere with.
  • Anti-malware tools that need to be hard to disable. A security daemon signed at the AntiMalware tier (pip_type = Protected, pip_trust = 1536 in the catalog) is protected against attempted disablement by malware running at lower trust.
  • Cryptographically signed third-party services that need protection from local administrators. A licensed-software service whose vendor has signed it at the App or Authenticode tier can be configured to refuse modification by anyone other than at the same or higher tier.

And patterns where PIP is not the answer:

  • Sandboxing untrusted code. PIP protects high-trust processes from low-trust ones, not the other way around. To restrict what an untrusted process can do, use confinement or restricted tokens.
  • Authorising users for specific operations. PIP is about binaries; identity-based authorisation is the DACL's job.
  • Network access control. PIP does not gate network operations; nftables, the network stack, and the SDs on network endpoints do.

The cleanest mental check: ask "is the policy I want to express about what kind of program may do this?". If yes, PIP. If it is about who may do this, the DACL is the answer.

Where to go next #

For how a binary comes to carry a PIP level at all — signatures, verification at exec, and pinning — read Binary signing.

For the rule that gives PIP its bite on every cross-process operation, read The two-check rule.

Peios Learn — documentation for the Peios project.

Built with Trail.