Process Mitigations
Single-page view · as markdown
Process mitigations
Peios / Peios Security Fundamentals / Process Mitigations
A mitigation is a per-process kernel-enforced hardening rule. Where the DACL, PIP, and the access check decide what objects a process may reach, mitigations decide what a process may do with itself — what regions of its own memory may be made executable, how its address space is laid out, what kinds of indirect control flow are permitted, what it can do with child processes.
Mitigations are stored on the process's PSB (Process Security Block) as a set of boolean flags. The PSB is the same per-process structure that holds the PIP fields and the process SD — covered in Process integrity protection. Each mitigation has its own flag; each flag controls one specific kernel-enforced behaviour.
The mitigation model is one-way: each flag can be turned on but never turned off. Once a process has enabled WXP (write-XOR-execute), it cannot disable WXP. The flag survives exec, so a child binary launched into a process that had enabled the mitigation inherits the constraint.
This page covers the model — what mitigations are, how they fit in alongside the other access-control layers, what the one-way and exec-preservation rules mean, and who sets them.
What mitigations protect against #
Mitigations exist for a different threat model than access control. The DACL protects an object from unauthorised callers; the access check decides whether the caller has the right to act. Mitigations protect a process from its own bugs and from injected code.
The motivating scenario for most mitigations is the same: a process has a memory-safety bug — a buffer overflow, a use-after-free, an integer overflow — that an attacker exploits to redirect execution. The exploit typically wants to do one of:
- Inject new executable code (shellcode) into the process and jump to it.
- Reuse existing code in unexpected ways (return-oriented programming, jump-oriented programming).
- Hijack indirect branches to land at attacker-chosen targets.
- Modify already-loaded code in place.
Each mitigation closes one or more of these doors. The mechanism is uniform: the kernel refuses the request that would enable the exploit, even though the syscall or memory operation looks legitimate. A process that has enabled WXP cannot mmap a writable-and-executable page; the kernel returns an error. A process with TLP enabled cannot mmap-as-executable a file outside the approved-paths cache; the kernel refuses.
The result is not "the bug is fixed" — the bug is still there, and the exploit may still be able to corrupt memory. What changes is what the exploit can do with the corrupted memory. A successful exploit on a process without mitigations gives the attacker arbitrary code execution; the same exploit on a process with mitigations gives the attacker a crashed process (the kernel refused the operation and the process aborts).
How mitigations differ from access control #
The two layers solve different problems:
| Access control | Mitigations | |
|---|---|---|
| What it gates | Operations against other objects (files, registry keys, processes) | Operations the process performs on its own memory and address space |
| Driven by | Identity and policy (token, SD, privileges) | Hardening posture chosen at process startup |
| Granularity | Per-object | Per-process |
| Adjustability | Identity can adjust within rules (AdjustPrivileges) | One-way; only ever tightened |
| Who sets it | authd (token), object owner / administrator (SD) | The launching process (typically peinit) or the process itself |
| Threat model | Untrusted callers reaching trusted objects | Code-execution exploits in the process's own memory |
A process can be subject to both layers simultaneously. A TCB daemon has restrictive access control (only TCB-level callers can interact with it) and a strict set of mitigations (WXP, LSV, TLP, CFI, PIE, SML). The two layers reinforce each other: access control keeps untrusted callers out, mitigations keep the process from being exploited even if untrusted input does reach it.
A process can also be subject to one without the other. An unprotected user-mode binary has no PIP, an open DACL, and no mitigations — it depends on the access control of objects it touches but has no internal hardening. The opposite — strict mitigations with permissive access — is less common in practice but legal.
The PSB storage and the one-way rule #
The mitigations live in a small bitfield on the PSB:
| Flag | Bit value |
|---|---|
| WXP | 0x001 |
| TLP | 0x002 |
| LSV | 0x004 |
| CFI (legacy alias for CFIF + CFIB) | 0x008 |
| UI_ACCESS | 0x010 (reserved) |
| NO_CHILD | 0x020 |
| CFIF | 0x040 |
| CFIB | 0x080 |
| PIE | 0x100 |
| SML | 0x200 |
| ALL | 0x3FF |
Each bit is independent. Setting a bit enables the mitigation; the bit can be set but never cleared. The kernel rejects any operation that would clear a previously-set bit.
The one-way rule is what makes mitigations trustworthy. A process that has WXP enabled cannot be tricked or coerced into disabling it. There is no syscall to clear a mitigation; there is no privilege that bypasses the rule. Once on, on for the lifetime of the process (and beyond — see below).
The same applies to NO_CHILD — bit 0x020 in the same bitfield. Once set, the process can never fork or clone again. There is no way to undo it.
Exec preservation #
When a process execs a new binary, almost everything about the process resets. The address space is wiped, the new binary is mapped, the entry point runs. But the PSB's mitigations survive exec. A process that had WXP enabled before exec still has WXP enabled after; the new binary inherits the constraint.
This is the rule that makes the one-way model genuinely one-way. Without exec preservation, an attacker who could control what binary the process execs could trivially "unset" the mitigations by exec'ing a binary in a fresh address space — but the kernel does not give the attacker that escape. The flags travel with the process, not with the binary.
The corollary: a binary that fundamentally cannot operate under a given mitigation (a JIT compiler under WXP, say) cannot be exec'd into a process that has that mitigation set. The exec will succeed (the kernel does not gate exec on mitigations), but the binary's first attempt to do the thing it needs (mmap PROT_EXEC of newly-written code) will fail with the appropriate error. The result is a process that runs the binary's startup code and then crashes when it tries to do its job.
Practical implication: deciding which mitigations to enable is a per-process decision made at startup, based on knowledge of what binary will run there. peinit knows that authd cannot tolerate WXP-incompatible operations because authd was compiled to be WXP-compatible. So peinit sets WXP on the authd process. A process launching arbitrary user binaries cannot make the same assumption; setting WXP on a user shell would break any JIT or self-modifying binary the user happened to run.
Who sets mitigations #
Three patterns for setting mitigations:
- peinit, before exec. When peinit launches a service, it forks, sets the desired mitigations on the child's PSB via
kacs_set_psb, then execs the service binary. The service comes up with the mitigations already in place. This is the standard pattern for system services. - The process itself, after startup. A process can set mitigations on its own PSB. The typical pattern is "after the early-startup work (which may need to relax some constraints), set the mitigations and continue with the constrained code". Self-application of mitigations is a hardening best practice for binaries that have a clear startup-then-steady-state split.
- A privileged supervisor. A process with
PROCESS_SET_INFORMATIONon the target and the appropriate PIP dominance can set mitigations on another process. Rare in practice — most mitigation-setting is either at exec time (peinit) or by the process itself.
In all three cases, the call is kacs_set_psb. The full mechanics — what fields can be set, what fails, who needs what privilege — are in Applying and lifecycle.
What the kernel actually does when a mitigation fires #
Each mitigation has its own enforcement points. The pattern is the same: a syscall the process is about to make is checked against the relevant mitigation; if the operation would violate the mitigation, the kernel returns an error (typically -EACCES or -EPERM) rather than performing the operation.
The process can then handle the error. In most cases, encountering a mitigation-blocked operation is unexpected — the process did not anticipate it could happen — and the result is a crash. In some cases, the process handles the error gracefully by falling back to a different code path. The kernel does not decide which; it just refuses the operation.
This is uniform across mitigations:
- WXP refuses
mprotectcalls that would transition pages W→X. - LSV refuses
mmap(PROT_EXEC)of unsigned or insufficiently-trusted files. - TLP refuses
mprotect(PROT_EXEC)of pages backing files outside approved paths. - CFIF refuses indirect branches that land outside ENDBR (or equivalent) target instructions.
- PIE refuses exec of non-PIE binaries (this one fires at exec, not at runtime).
Each is enforced by the kernel at the syscall layer. There is no userspace component. The process cannot bypass them by avoiding libc; the syscalls themselves carry the check.
Where to start #
If you want the catalog — what each individual mitigation does, when it fires, what kernel surfaces it covers — read Catalog.
If you want the operational mechanics — kacs_set_psb, the right syscall and privilege requirements, the lifecycle of a mitigation flag from initial set through fork and exec — read Applying and lifecycle.
Catalog
Peios / Peios Security Fundamentals / Process Mitigations
There are eight mitigations active in v0.20 plus a reserved slot. This page covers each one. Every mitigation works the same way structurally — a flag on the PSB; the kernel checks the flag at the syscall layer; offending operations are refused — but each gates a different kind of operation. Understanding the catalog is understanding which threats each closes off. The numeric KACS_MIT_* flag values are cataloged in Other constants.
LSV — Library Signature Verification #
LSV is the mitigation that decides which executable code may be loaded into the process from disk. With LSV enabled, mmap(PROT_EXEC) requires that the file backing the mapping carry a valid signature whose PIP trust level is at least the calling process's pip_trust. Unsigned files cannot be loaded executable; files signed at a lower trust level cannot either.
The kernel's behaviour:
- When
mmap(..., PROT_EXEC, ...)is called on a file fd, the kernel checks the file's signature. - If the file has no signature, or its signature is invalid, the call fails with
-EACCES. - If the file's signature is valid but its PIP trust level is below the calling process's, the call also fails with
-EACCES. - If the file's signature is valid and at sufficient trust, the call proceeds.
The result: a TCB process (pip_trust = 8192) can only load TCB-signed libraries. A future App-signed process (pip_trust = 2048) could load App-signed or higher libraries but not Authenticode-signed or unsigned ones. A None-trust process is not subject to LSV (LSV does not check against None; the process can load anything).
LSV is the mitigation that closes the "load arbitrary shared object" injection path. An attacker who has overwritten a function pointer in the process to point at dlopen("evil.so") finds that the call fails — the kernel refuses to map the unsigned library as executable.
LSV is appropriate for any PIP-protected process. peinit, authd, and the rest of the TCB all run with LSV enabled.
WXP — Write-XOR-Execute #
WXP refuses any operation that would transition a memory page from writable to executable. Pages can be writable, or executable, but not both at any moment, and not become executable having been writable.
Specifically:
mmap(..., PROT_WRITE | PROT_EXEC, ...)is refused if WXP is enabled.mprotect(addr, len, PROT_EXEC)on pages previously protected with PROT_WRITE is refused.mprotect(addr, len, PROT_WRITE)on pages previously protected with PROT_EXEC is refused.
The check fires per-page. The kernel tracks the protection history of each mapping; a page that has ever been writable cannot subsequently be made executable, and vice versa.
The effect: JITs (Just-In-Time compilers) cannot run with WXP enabled. A JIT's whole job is to allocate a region of writable memory, generate code into it, then flip the region executable — exactly what WXP refuses. Binaries that need this flexibility (managed-language runtimes, dynamic-recompilation engines) cannot be hardened with WXP.
Binaries that do not generate code at runtime are unaffected. A normal native binary loads its code from disk at exec (PROT_EXEC granted by exec, not by mprotect), uses stack and heap as PROT_READ | PROT_WRITE, and never needs to flip pages between write and execute. WXP is invisible to such binaries.
WXP is one of the cornerstones of process hardening. It closes the "inject shellcode and jump to it" pathway: an attacker who has corrupted memory cannot make their corrupted region executable. The exploit's payload, no matter how big, is just data.
TLP — Trusted Library Paths #
TLP gates which directories executable mappings may come from. The kernel maintains a per-system cache of approved directory prefixes (populated from the registry at boot); when TLP is enabled, a process can only mprotect(PROT_EXEC) (or mmap(PROT_EXEC)) a region backed by a file whose resolved absolute path begins with one of those prefixes.
The check:
- The kernel resolves the file's path to an absolute pathname.
- The absolute path is compared against each entry in the TLP cache.
- If the path begins with any entry's prefix, the operation proceeds.
- If not, the operation fails with
-EACCES.
The TLP cache holds typical system library paths: /usr/lib/, /lib/, perhaps a few service-specific paths. Anything in /tmp/, /home/, or in directories not explicitly in the cache is excluded.
The threat TLP closes is "load executable code from a writable directory". A code-injection attack that writes a shared object into /tmp/ and loads it via dlopen finds the load refused — /tmp/ is not in the TLP cache. The same attack that puts the file in /usr/lib/ is much harder; non-administrators cannot write to /usr/lib/ in a standard configuration.
TLP composes naturally with LSV. LSV says "the library must be signed at sufficient trust"; TLP says "the library must come from an approved location". Both are typically enabled together on hardened processes; either alone is a useful but partial defence.
TLP cache details:
- Maximum 64 entries.
- Each entry is a UTF-8 absolute directory prefix, beginning and ending with
/. - Maximum 4096 bytes per path.
- Populated by peinit at boot from the registry. The cache is machine-wide; every process sees the same prefixes.
CFIF and CFIB — Control Flow Integrity (Forward and Backward) #
CFIF (Forward) and CFIB (Backward) are the two halves of control-flow integrity. They are separate flags so a process can enable one without the other, though most hardened processes enable both. The combined effect is to refuse any indirect branch (forward or return) that lands at an unintended target.
The legacy flag CFI (0x008) is an alias that sets both CFIF and CFIB. Modern code should set CFIF (0x040) and CFIB (0x080) directly; the alias exists for compatibility.
CFIF — Forward CFI #
CFIF refuses indirect calls and jumps (function pointers, vtables) that land at an instruction other than a designated call target. On x86_64, the hardware mechanism is IBT (Indirect Branch Tracking) — every legitimate target of an indirect branch is marked with an ENDBR64 instruction; an indirect branch landing somewhere without ENDBR64 traps.
With CFIF enabled, the kernel ensures the process runs in IBT-enforcing mode. An indirect branch to a non-ENDBR64 target generates a control-protection fault, which the kernel turns into a fatal signal to the process.
The threat closed: ROP/JOP (Return-Oriented / Jump-Oriented Programming) attacks that chain together short "gadgets" found in legitimate code. The gadgets are short sequences ending in ret or indirect jump; without CFIF, an attacker can use them to perform arbitrary operations. With CFIF, the gadgets are no longer reachable because the indirect branch into them lands somewhere without ENDBR64.
CFIB — Backward CFI #
CFIB refuses ret instructions that do not return to the address pushed by the corresponding call. The hardware mechanism is the shadow stack — a separate stack maintained by the CPU that records return addresses. Every call pushes onto both the data stack and the shadow stack; every ret pops both and compares them. A mismatch traps.
With CFIB enabled, the kernel ensures the process runs with the shadow stack engaged. An attacker who overwrites a return address on the data stack cannot get the ret to honour their overwrite — the shadow stack still has the original address; the comparison fails; the process dies.
The threat closed: classic ROP. Overwriting return addresses is the foundation of return-oriented exploitation; CFIB makes the trick impossible (or, more precisely, makes it lead to immediate process termination instead of attacker-chosen execution).
Both CFIF and CFIB require hardware support (Intel CET, ARM BTI/PAC, etc.) plus binary support (the binary must have been compiled with the relevant flags). Enabling CFIF or CFIB on a process whose binary does not support it has no effect — the kernel can only enforce what the hardware can detect.
PIE — Position-Independent Executable #
PIE refuses exec of binaries that are not position-independent. The kernel checks the binary's ELF flags at exec; if PIE is enabled on the parent process's PSB and the new binary is not PIE-built, the exec fails with -EACCES.
PIE-built binaries are loaded at randomised addresses every time they exec — Address Space Layout Randomisation (ASLR) covers the executable's own segments, not just the heap and shared libraries. An attacker who would have needed to know the address of a specific instruction in the binary to construct a ROP chain finds the address randomised and unpredictable.
A non-PIE binary has fixed addresses for its code and globals. Every exec puts them in the same place. An attacker can compute exploitation gadget addresses once and reuse them across runs.
PIE is the easiest mitigation to enable: any modern compilation with -fPIE -pie produces a PIE binary. Distributions of Peios produce PIE binaries by default. The mitigation simply refuses to exec the rare exception.
The cost: a small (single-digit-percent) performance overhead because PIE binaries reference memory through GOT/PLT tables rather than direct addresses. The cost is dwarfed by the security benefit on any modern CPU.
SML — Speculation Mitigation Lock #
SML enables the most paranoid set of speculative-execution mitigations for the process — Spectre, Meltdown, MDS, and the rest of the side-channel family. With SML enabled, the kernel applies all relevant mitigations on every context switch into and out of this process: indirect-branch barriers, store buffers cleared, microcode flushes, anything the CPU supports for spectre-class defences.
The cost is significant — depending on the CPU and the workload, anywhere from a few percent to tens of percent of performance. Most workloads do not need it. The processes that do need it are the ones handling secrets that an attacker on the same machine could otherwise extract through speculative-execution side channels: cryptographic key holders, sealed-secret stores, the TCB processes that hold sensitive material.
SML is per-process. A SML-enabled process pays the cost; processes without SML in the same kernel use the default (lighter) set of mitigations.
The threat SML closes: an attacker who has unprivileged code running on the same machine (or in some configurations, even on a different machine sharing the same CPU) using speculative-execution timing side channels to extract data from a victim process. Without SML, the standard mitigations may be enough for most attacks; with SML, the process is hardened against even the most subtle.
NO_CHILD — Forbid fork and clone #
NO_CHILD refuses fork, clone, and any other path that would create a new process or new thread sharing the current address space. (CLONE_THREAD-style clones to add threads to the current process are not covered by NO_CHILD; the flag only refuses new processes.)
With NO_CHILD enabled:
fork()returns-EPERM.clone()returning a new process returns-EPERM.clone3()with similar flags returns-EPERM.
The threat closed: an exploit that has gained code execution in the process attempting to spawn a helper process to do its work. NO_CHILD makes that path impossible — the process is locked into being a single process; whatever the attacker does, they cannot fork out of it.
NO_CHILD is appropriate for processes that fundamentally do not need to fork. Many long-lived services (a single-process event-loop daemon, for example) never call fork in their normal operation. Setting NO_CHILD on such a service costs nothing operationally and closes a frequently-abused exploitation path.
Services that do fork during their operation (a classical Unix accept-fork-handle server) cannot use NO_CHILD. Either restructure the service to use threads or async I/O, or accept that NO_CHILD does not fit.
NO_CHILD does not prevent threads in the same process — pthread_create and its kin are CLONE_THREAD-style operations and remain available. The mitigation is specifically about new processes, not new threads.
UI_ACCESS — Reserved #
UI_ACCESS (0x010) is reserved in v0.20. The intended use is for processes that interact with the user interface in privileged ways; the mitigation, when defined, will restrict what UI surfaces the process may attach to.
In v0.20 the flag has no effect — setting it is allowed but does nothing. Future versions may define behaviour.
Combining mitigations #
Most hardened processes enable multiple mitigations. A typical TCB daemon's mitigation set in v0.20:
- WXP — no writable-executable pages
- LSV — only signed libraries
- TLP — only libraries from approved paths
- CFIF + CFIB — control-flow integrity
- PIE — ASLR-aware binaries
- NO_CHILD if applicable — no child processes
Plus possibly SML for secrets-handling processes.
The mitigations compose orthogonally — each one closes a different attack pathway. WXP closes shellcode injection; LSV closes signed-library-only-rule; TLP closes load-from-writable-directory; CFI closes ROP/JOP; PIE closes fixed-address attacks; NO_CHILD closes process-spawning escapes; SML closes speculative-execution side channels. A process with all of them is meaningfully harder to exploit than one with any subset.
The ALL flag (0x3FF) is shorthand for "enable every defined mitigation". A process that wants the strictest possible hardening can set ALL in one call.
What mitigations don't help with #
A few clarifications worth pinning:
- Mitigations do not protect against logic bugs. A process that has been tricked into doing something its code is allowed to do but should not (a misconfigured permission check, a misused API) is not protected by mitigations.
- Mitigations do not protect against bugs in the kernel. A kernel exploit operates above the mitigation layer; mitigations are kernel-enforced, and a compromised kernel can disable them.
- Mitigations do not protect against bugs in the language runtime that bypass them. A JIT that legitimately needs writable-executable pages is incompatible with WXP; running it with WXP either disables the JIT (which may fail in unexpected ways) or refuses to set WXP at all.
- Mitigations do not catch all exploits. They are layered defences. An exploit that fits within one mitigation's blind spot (a JIT spray attack against a non-WXP process, say) succeeds despite the other mitigations being on. Defence in depth is the goal — the more layers, the more an exploit must defeat.
See also #
- Process mitigations — the model these flags share.
- Applying and lifecycle — setting the flags and how they propagate.
- Binary signing — the signatures LSV verifies against.
Applying and lifecycle
Peios / Peios Security Fundamentals / Process Mitigations
Setting mitigations is a small kernel operation — call a syscall, pass a bitmask, the kernel sets the bits on the PSB. The complications are not in the call itself; they are in who can make the call, when in a process's life it can be made, and how the resulting state propagates through fork and exec.
This page covers the operational mechanics: the kacs_set_psb syscall, the privilege rules for setting mitigations on another process, and the lifecycle of a mitigation flag from initial set through process exit.
kacs_set_psb #
The kernel exposes one syscall for setting mitigations:
kacs_set_psb(target_pidfd, flags)
target_pidfd is a pidfd for the process whose PSB is to be modified. The caller's own process is acceptable; another process is also acceptable (subject to privilege rules below).
flags is a bitmask combining the values from the catalog:
| Flag | Bit |
|---|---|
| WXP | 0x001 |
| TLP | 0x002 |
| LSV | 0x004 |
| CFI (legacy alias) | 0x008 |
| UI_ACCESS | 0x010 |
| NO_CHILD | 0x020 |
| CFIF | 0x040 |
| CFIB | 0x080 |
| PIE | 0x100 |
| SML | 0x200 |
| ALL | 0x3FF |
The kernel ORs the flags into the target's existing mitigation bitfield. There is no "clear" — bits cannot be removed. Calling kacs_set_psb with a subset of currently-set bits leaves the previously-set bits intact; you can never use this call to disable a mitigation.
The kernel rejects:
- A pidfd pointing at a process the caller does not have authority to modify (see below).
- Unknown flag bits (anything outside the defined set is
-EINVAL).
The kernel does not reject:
- A call setting bits that are already set. The OR is idempotent.
- A call setting
UI_ACCESS(which is reserved). It sets the bit; the bit has no effect. - A call setting flags incompatible with the current binary's capabilities. Setting WXP on a process running a JIT is allowed; the JIT will fail the next time it tries to flip a page, but the
kacs_set_psbcall itself succeeds.
Self versus another process #
The privilege required depends on whose PSB is being modified:
Setting mitigations on your own process requires no privilege. Any process can call kacs_set_psb with its own pidfd (or, more commonly, with no pidfd to mean "self"). The call always succeeds for self-targeted invocations, regardless of identity, integrity, or anything else.
The reasoning: a process can only ever tighten its own constraints. There is no risk in letting a process restrict itself further. The model assumes that any code running in the process is, by definition, code the process has chosen to run; that code wanting to add a mitigation is fine.
Setting mitigations on another process requires:
PROCESS_SET_INFORMATIONon the target process — granted by the target's process SD.- PIP dominance over the target (per the two-check rule).
This is the standard cross-process operation pattern. The caller's PSB must dominate the target's, and the target's SD must grant the appropriate right to the caller. Both must be satisfied.
In practice this means the only common caller for cross-process kacs_set_psb is peinit (when launching a service that needs mitigations applied at exec). peinit has TCB-level PIP and is granted PROCESS_SET_INFORMATION on the services it launches; it sets the desired mitigations on the child's PSB after fork and before exec.
A self-applied mitigation does not require the caller to be the process itself in the strict sense — it just requires the pidfd to point at the caller's own process. A thread within a process can set mitigations on the process's PSB regardless of which thread does the call.
Where in the process lifecycle #
A mitigation can be set at any moment during a process's life. The kernel does not require it to happen at startup, before exec, or before any specific event. Practical patterns:
- At process creation, before exec. peinit forks, calls
kacs_set_psbon the child's pidfd, then execs the service binary. The mitigations are in place when the binary starts running. This is the standard. - At the entry point of the binary. The binary itself, immediately on startup, sets its desired mitigations on its own PSB. Suitable for binaries that are self-aware about their hardening posture.
- After early-stage initialisation. A process that has work to do during early startup that needs to relax some constraints (loading executable libraries from non-approved paths, for example) waits until after that work is done, then sets the mitigation.
A mitigation set late in a process's life closes off only future operations. Operations that have already happened — pages already mapped, libraries already loaded — are not retroactively checked. WXP set after the process has already mmap'd a writable-executable region does not unmap that region; it only refuses future such operations.
This is sometimes a useful pattern: a process needs WXP-incompatible behaviour during startup (say, runtime code generation for initialisation) and then transitions to a steady state where WXP is appropriate. The pattern is "do the WXP-incompatible work first, then kacs_set_psb(self, WXP)". From that point forward, WXP is enforced.
Fork: inheritance #
When a process forks, the child inherits the parent's mitigation flags exactly. Every bit set on the parent is set on the child. The child cannot un-set them at fork time; the one-way rule applies.
This is the natural extension of one-way: a process that has chosen to lock down its execution cannot give its children more authority than it has itself. If WXP is set, every child also has WXP. If NO_CHILD is set... well, the parent cannot fork in the first place, so the question does not arise.
Threads (CLONE_THREAD-style clones) share the parent's PSB rather than copying it. A new thread in the same process is bound by the same mitigations; setting a mitigation in one thread is visible to all threads immediately.
Exec: preservation, with one wrinkle #
When a process execs, all of:
- The process identity (token) — preserved.
- The PIP fields — re-computed from the new binary's signature.
- The process SD — typically preserved, may be re-defaulted if the user identity changed.
- The mitigation flags — preserved.
The new binary runs with whatever mitigations were on the PSB before exec. There is no way for exec to relax mitigations.
The one wrinkle: PIE.
PIE is the mitigation that fires at exec, not at runtime. With PIE set, the kernel checks the new binary's ELF flags during exec; if the binary is not PIE-built, the exec fails with -EACCES. The process attempting the exec sees its execve return with an error and continues running its current binary.
For other mitigations, the exec succeeds and the new binary inherits the mitigation. PIE is the one that can cause exec itself to fail.
This means setting PIE before launching a service is a way of saying "this service binary must be PIE, or it cannot run". If the operator updates the service to a binary that is not PIE, the next exec attempt will fail and the service will not start. This is sometimes desired (a hard constraint that the binary be PIE); sometimes inconvenient (an emergency rebuild that lost the PIE flag).
NO_CHILD and the lifecycle interaction #
NO_CHILD (bit 0x020) is also stored on the PSB and follows the same one-way rules as the other mitigations. Once set, the process cannot fork or clone-with-new-process.
The lifecycle interaction worth knowing: a process that wants to set up children and then lock itself down should do the forks first, then call kacs_set_psb(self, NO_CHILD). After that call, the process cannot create more processes.
If a process needs to be able to fork on demand (a server that handles each connection in a new process), NO_CHILD is not appropriate. The fork capability and NO_CHILD are mutually exclusive in steady state.
A process with NO_CHILD set can still call exec (replacing itself with a new binary in the same process) and create threads via CLONE_THREAD. The mitigation specifically blocks the spawning of new processes.
Querying mitigations #
A process can read its own mitigation flags via the PSB query. The interface — typically through kacs_open_self_token and a query on the PSB — returns the current flag bitfield.
For reading another process's flags, the same PROCESS_QUERY_INFORMATION + PIP dominance rules apply as for setting them. Typically only peinit or a debug tool would read another process's mitigation flags.
Note that the flags are independent of the PSB's other fields. Querying the mitigation flags does not reveal anything about the process's PIP level or its SD — those are separate queries. It does include NO_CHILD, which is bit 0x020 of the same bitfield.
What happens at process exit #
A process's PSB is destroyed when the process exits. The mitigation flags vanish with it. There is no persistence; the next time the same binary is exec'd in a fresh process, the mitigations have to be re-applied.
This is why launchers (peinit) apply mitigations on every launch. There is no cached "this binary always gets these mitigations" — every fresh process starts from the inherited PSB, which is whatever the launcher's PSB had plus whatever the launcher chose to add.
The corollary: a process whose launcher does not apply mitigations runs without them, regardless of the binary's intent. A binary that wants to be hardened should also call kacs_set_psb at its own entry point so the mitigations are guaranteed regardless of who launched it. Defence in depth: both the launcher and the binary should set the mitigations they need.
Errors #
kacs_set_psb can fail with:
| Error | Cause |
|---|---|
-EBADF | Invalid pidfd. |
-ESRCH | The target process has exited. |
-EACCES | The caller does not have PROCESS_SET_INFORMATION on the target. |
-EPERM | The caller does not PIP-dominate the target (when modifying another process). |
-EINVAL | Unknown flag bits in flags. |
In normal operation, the call succeeds. Failures are typically programming errors (wrong pidfd) or insufficient authority (a low-trust caller trying to modify a high-trust target).
Where to go next #
For what each flag actually enforces once set, read the Catalog.
For the SD-plus-dominance rules that gate setting mitigations on another process, read The two-check rule.