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 replacement binary runs under the process's existing constraints. Children inherit committed flags at fork.
This page covers the model: what mitigations are, how they fit alongside access control, and why lifecycle boundaries matter. For an operational check, start with Applying and lifecycle: inspect the running process, compare its committed flags with the required policy, and keep a failed request separate from verified protection.
A flag in a catalog or launch configuration is not proof it is active. The Kernel TRM describes activation-backed, all-or-nothing application and current implementation limits. In particular, it documents CFIF as unavailable and CFIB activation as self-only. Check those limits before interpreting a requested hardening set.
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. An exploit may be stopped at a refused operation or cause a process fault instead of achieving code execution. That is a defence against particular exploitation paths, not a guarantee that every exploit is stopped or that every refusal crashes the process.
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 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 can have restrictive access control and a required hardening policy covering memory, libraries, control flow, address layout and speculation. Its actual mitigation set still depends on successful activation; neither its signing level nor the desired policy proves those flags are on. 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.
PIP, the process descriptor and mitigations are independent dimensions. A process without PIP can still have a restrictive descriptor and committed mitigations. For example, exec of an unsigned binary can remove PIP while preserving the process descriptor and mitigation flags. Inspect each relevant surface rather than inferring an open DACL or absent hardening from the lack of PIP.
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 |
The table identifies the request bits, not a set that every platform can enable. CFI expands to CFIF and CFIB and is not stored as a separate active flag; UI_ACCESS is reserved; ALL is the accepted-request mask. The ABI catalog is the numeric reference.
Before committing a new mitigation, the kernel must activate or verify its protection. If any requested protection fails, the request changes no bits. Previously committed flags cannot be cleared or weakened; requesting only a subset does not remove the rest.
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 cannot fork or clone a new process. Creating threads with CLONE_THREAD remains possible. There is no way to clear the restriction.
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.
Preservation is not a promise that the new binary will work. PIE explicitly rejects a non-PIE binary at exec. Other constraints may reject operations the new program needs, such as a JIT attempting to make writable code executable under WXP, or a library load failing LSV or TLP. Check the actual failing operation rather than assuming exec always succeeds and only later operations can fail.
Practical implication: choose the policy with knowledge of the program and its launch path. A service launcher can request the protections its service requires and check the result. A launcher of arbitrary user binaries cannot assume every program is compatible: applying WXP to a shell's process lineage can prevent JIT or self-modifying programs from working.
Who sets mitigations #
Three patterns for setting mitigations:
- The launcher, before exec. The launcher's code running in the newly forked child can request compatible mitigations before exec and check the result. This is the between-fork-and-exec pattern described by the Kernel TRM. It is self-application in the child, distinct from the parent modifying another task.
- The process itself, during startup. A program can request protections for its own PSB, preferably before untrusted input. Late application must validate existing state; it cannot exempt incompatible startup mappings simply because they were created before the request.
- An authorised supervisor. A caller with
PROCESS_SET_INFORMATIONand PIP dominance can request changes to another process. Authority does not bypass activation limits: the Kernel TRM documents CFIB as self-only and CFIF as unavailable.
These are application patterns, not evidence of the service launcher's behavior. The source-checked peinit 0.0.12 launch path does not apply an additional per-service mitigation mask. Inherited or self-applied flags may still be present; inspect the running process rather than inferring protection from its launcher's identity.
In each case, success must be checked and committed state verified. Applying and lifecycle explains that evidence; the existing SDK reference holds the programming interface.
What the kernel actually does when a mitigation fires #
Each mitigation has its own enforcement points. Memory and process-operation checks can refuse a syscall with an error such as EACCES or EPERM. Architecture-backed protections operate through hardware and kernel support; a control-flow violation can fault rather than return a syscall error.
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.
The protections address different operations:
- 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. - Forward CFI is intended to reject indirect branches outside designated targets such as ENDBR instructions. The Kernel TRM currently documents CFIF activation as unavailable; do not count it as active merely because it was requested.
- PIE refuses exec of non-PIE binaries (this one fires at exec, not at runtime).
These protections depend on kernel enforcement and, where applicable, architecture support rather than cooperation from libc. Avoiding libc does not bypass kernel checks. A named protection still has to be supported and successfully committed before it can be relied on.
Where to start #
If you want the catalog — what each individual mitigation does, when it fires, what kernel surfaces it covers — read Catalog.
To inspect actual hardening, understand a refused request, or check what survives fork and exec, read Applying and lifecycle. Use its links to the command, SDK and Kernel TRM references for the corresponding interfaces.
Catalog
Peios / Peios Security Fundamentals / Process Mitigations
This catalog covers eight named protections plus the reserved UI_ACCESS slot. A defined flag is not a guarantee that it can be activated on a given process or platform. The Kernel TRM documents activation-backed, all-or-nothing requests and current implementation limits; Applying and lifecycle explains how to inspect the committed state.
The entries retain the threat model of each protection. Read the support and lifecycle limits alongside it before choosing a service policy. The numeric KACS_MIT_* values are cataloged in Other constants and the Kernel ABI.
LSV — Library Signature Verification #
LSV decides which file-backed executable code may be loaded into the process. It requires a valid signature. For a PIP-protected process, the library must also meet the process's PIP trust floor; for a process with no PIP protection, a valid signature is still required but the trust floor is not compared.
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 caller is PIP-protected and the file's valid signature is below its required trust, the call also fails with
-EACCES. - If the file's signature is valid and meets the applicable trust requirement, it passes the LSV check. Other mitigation or access checks may still refuse the operation.
A TCB process (pip_trust = 8192) can therefore load only libraries meeting that trust. The conceptual tier model also describes an App tier (2048) that would need App-or-higher libraries rather than Authenticode or unsigned ones; the Kernel TRM currently documents only Protected/PeiosTcb as producible. Do not use a hypothetical tier as evidence of deployed support. A process with no PIP protection is still subject to LSV signature checks when the flag is on.
Activation also validates existing file-backed executable mappings. A process cannot first load unsigned code and then successfully commit LSV over it. Adding executable permission with mprotect runs the same verification; anonymous executable mappings are governed by WXP rather than LSV. See Library Signature Verification.
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 relevant to PIP-protected services such as peinit and authd, but the service name or signing tier is not proof that it is enabled. Check the required policy, library compatibility and committed state.
WXP — Write-XOR-Execute #
WXP rejects writable-and-executable mappings and transitions between writable and executable protection. These are the invariants documented by the Kernel TRM.
Specifically:
mmap(..., PROT_WRITE | PROT_EXEC, ...)is refused if WXP is enabled.- An
mprotecttransition from writable to executable is refused. - An
mprotecttransition from executable to writable is refused.
The Kernel TRM establishes transition checks and validation of observable existing state. It does not establish a lifetime history of every page's earlier protections; do not rely on that stronger model when assessing compatibility.
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.
A native binary that loads its executable code from disk and uses writable, non-executable stack and heap may not need these transitions. That is a compatibility consideration, not proof that every native binary is unaffected: review the program's actual mapping requirements.
WXP activation also checks existing mappings: the Kernel TRM specifies failure for an existing writable-executable mapping or another observable invariant violation. A late request is not an exemption for code generation done earlier.
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 the paths used for file-backed executable mappings. The kernel compares the resolved absolute backing path against a machine-wide cache of approved directory prefixes for mmap(PROT_EXEC) and when mprotect adds execute permission.
Current implementation limit: the Kernel TRM states that the cache has no production writer: no syscall, securityfs node or registry path fills it. Only a KUnit test helper populates it. The documented shipping cache is therefore empty, so it matches no path. Do not assume that peinit fills a usable library allowlist at boot or that editing the registry can do so.
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 an entry's prefix, it passes the TLP check. Other checks still apply.
- If not, the operation fails with
-EACCES.
Paths such as /usr/lib/, /lib/ or a service-specific directory illustrate possible prefixes, not installed cache entries. /tmp/, /home/ and every other path fail when none of the configured prefixes matches; with the documented empty production cache, even system-library paths fail.
The threat model is loading executable code from an untrusted location, such as an attacker writing a shared object into /tmp/ and loading it with dlopen. A useful path policy would allow only suitably protected directories; a trusted-looking pathname alone does not prove the directory cannot be modified. TLP checks the prefix, while filesystem access control determines who can write there.
TLP and LSV have complementary models: LSV requires an acceptable signature; TLP requires an approved location. That does not make their combined activation a safe default. TLP also validates existing file-backed executable mappings and fails if their paths cannot be resolved or accepted, including when the cache is empty. Anonymous mappings are outside TLP and LSV; WXP governs them.
TLP cache details:
- Maximum 64 entries.
- Each entry is an absolute directory-prefix byte string, beginning and ending with
/, checked against kernel-resolved path bytes. The trailing slash prevents/usr/lib/from matching/usr/libevil. - Maximum 4096 bytes per path.
- The cache is machine-wide. Empty, relative, NUL-containing or non-slash-terminated prefixes are invalid; the Kernel TRM specifies atomic rejection of invalid updates. No supported production population route is documented.
CFIF and CFIB — Control Flow Integrity (Forward and Backward) #
CFIF (Forward) and CFIB (Backward) name separate control-flow protections: indirect-call/jump target checking and return-address checking. Do not assume both are available because both have flag values. The Kernel TRM currently documents CFIF as unavailable and CFIB activation as self-only.
The legacy request flag CFI (0x008) expands to CFIF (0x040) and CFIB (0x080); the alias is not stored in readback. Requesting the alias still requests the unavailable CFIF half, so it is not a shortcut around activation limits. Application code should name the specific supported protections its policy requires.
CFIF — Forward CFI #
The forward-CFI model rejects 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.
Such enforcement would require placing the process in IBT-enforcing mode, where an indirect branch to a non-ENDBR64 target raises a control-protection fault. The Kernel TRM says CFIF cannot currently be committed: activation against a live task returns ENODEV because no userspace IBT/BTI control surface is exposed. Treat a request as failed, not as silently active protection.
The threat model is code reuse through unintended indirect-call and jump targets, including JOP gadgets in legitimate code. Return-oriented programming uses return-address manipulation, covered by the separate backward-CFI model below. The presence of ENDBR instructions in a binary is not evidence that CFIF enforcement was activated.
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).
Hardware mechanisms such as Intel CET and architecture-specific BTI/PAC facilities illustrate why control-flow protection needs platform and program compatibility; they are not a cross-platform support guarantee. The Kernel TRM describes CFIB through shadow-stack enable-and-lock and rejects activation on a task other than the caller. A program must use a supported self-hardening route and check the result.
Unsupported architecture-backed activation fails closed. It is not a successful no-op on an incompatible platform or program. Do not infer protection from compiler flags alone; verify the committed mitigation state.
PIE — Position-Independent Executable #
PIE refuses exec of binaries that are not position-independent. The kernel checks the ELF image type at exec; if PIE is enabled on the 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 must be committed before the exec it is intended to constrain; setting it afterward does not prove that the current binary was checked at launch.
For build compatibility, PIE builds use compiler/linker options such as -fPIE -pie. Even when a distribution builds PIE binaries by default, verify the service's actual packaged binary; a local or emergency rebuild can lose the property.
Position-independent code can change addressing and indirection, including GOT/PLT use, with a performance cost that depends on the toolchain, architecture and workload. Do not use a fixed percentage as a compatibility or capacity guarantee.
SML — Speculation Mitigation Lock #
SML locks speculation mitigations on for the process. The threat family includes speculative-execution side channels such as Spectre, Meltdown and MDS; available defences and their cost depend on the platform. The Kernel TRM describes activation through the architecture's kernel interface, with failure if protection cannot be made effective. It also accepts a platform reporting speculation as unconditionally not affected, with nothing to enable. A committed SML flag is not a list of particular barriers, buffer clears or microcode operations applied on every context switch.
The performance cost can be significant and depends on the CPU and workload. Review the security requirement and capacity impact for secrets-handling processes such as cryptographic key holders, sealed-secret stores and TCB services; neither a fixed overhead estimate nor a blanket claim that a workload does not need SML establishes the right policy.
SML is a per-process commitment. The flag does not describe the complete system-wide speculation policy or the overhead borne by other processes.
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. SML prevents the process from disabling the documented protection; it does not establish that every side channel is eliminated.
NO_CHILD — Forbid fork and clone #
NO_CHILD refuses fork, clone without CLONE_THREAD, and other paths that create a new process. CLONE_THREAD operations that add threads to the current process are not covered. It constrains subsequent creation; it does not remove children that already exist.
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. The Kernel TRM describes its intended purpose as interaction with higher-integrity UI elements, for future desktop functionality.
Its presence in the accepted mask or in PSB readback is not evidence of an implemented UI protection. Do not include it as an effective defence in a hardening assessment.
Combining mitigations #
A hardening policy may address several independent needs:
- WXP — no writable-executable pages
- LSV — only signed libraries
- TLP — only libraries from approved paths
- CFIF and CFIB — forward and backward control-flow models, subject to the activation limits above
- PIE — ASLR-aware binaries
- NO_CHILD if applicable — no child processes
Plus possibly SML for secrets-handling processes.
These address different exploitation paths, but a policy must account for both compatibility and support. The current Kernel TRM's unavailable CFIF, self-only CFIB and empty production TLP cache prevent treating the list as a deployable default. Compare the service's required set with successful application and committed readback, not with an idealised list.
The ALL mask (0x3FF) identifies valid request bits. It is useful for input validation, not blanket hardening: it includes reserved and unavailable flags. Because requests are all-or-nothing, an unavailable requested protection prevents the whole request from committing. Do not substitute ALL for a reviewed set or silently omit a required protection to make a request succeed.
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
A requested mitigation mask is not evidence that the process is protected. Check the application's or launcher's result, then read the committed mitigation state of the process that is actually running. A service's intended policy, a flag's presence in the catalog, and a successful activation are different things.
For an operator investigating hardening:
- Identify the process and read it with
logonse psb --pid PID, replacingPIDwith the target's process ID. With only--pid, this is a read-only inspection; see the logonse reference. - Record the reported mitigations and process GUID alongside the service's required policy and any application error. The GUID distinguishes a replacement process from an earlier process with the same PID.
- Compare the committed flags with the required set and the activation limits. If a required flag is absent, or the report is refused or unavailable, do not treat the intended protection as verified.
- After an authorised launch or hardening change, check the result and read the same process again. Verify a new process separately after a restart; do not carry an old process's result forward.
This page explains the lifecycle and the limits of that evidence. Application code belongs in the SDK hardening guide and process.h reference; command syntax belongs in logonse. The Kernel TRM describes activation-backed behaviour and current implementation limits below. Use the documentation for the deployed release when checking support; this page does not establish when those implementation limits changed between releases.
Requests and committed protection #
kacs_set_psb is the kernel operation behind a mitigation request. It targets a process by pidfd and takes a mask of KACS_MIT_* flags. The SDK reference documents the callable wrapper, including -1 for the calling process; the kernel ABI holds the complete numeric flag table and syscall declaration.
A request is additive and one-way. It cannot clear an existing flag. Requesting an already-set flag or a subset of the committed set does not remove or weaken earlier protections. CFI is a legacy request alias for CFIF | CFIB; the alias itself is not retained in the committed bitfield. UI_ACCESS is reserved. ALL is the valid-bit mask, not a recommended policy or a promise that every defined flag can be activated.
The Kernel TRM specifies activation-backed, all-or-nothing application: before committing a new flag, the kernel activates its protection or verifies the required invariant. If any requested mitigation cannot be activated or verified, no bits from that request are changed. Bits committed by earlier successful requests remain set.
Unknown bits and an invalid or unauthorised target can be refused. Valid bits can also be refused because of existing mappings, platform support, or the activation route. A request made by the process itself is subject to these checks too.
Self versus another process #
Self-application needs neither PROCESS_SET_INFORMATION on another process nor PIP dominance over one. A thread can request hardening for its own process. This is permission to tighten its constraints, not an unconditional promise of success: activation and input validation still have to succeed.
Applying mitigations to another process requires both:
PROCESS_SET_INFORMATIONon the target's process security descriptor.- PIP dominance over the target, under the two-check rule.
Satisfying both gates does not make every mitigation available remotely. The Kernel TRM documents CFIB activation as self-only: enabling it on a task other than the caller fails. It also documents CFIF activation against a live task as unconditionally returning ENODEV, because there is no userspace IBT/BTI control surface. A supervisor cannot overcome these activation limits merely by having authority over the target.
For a launcher using this pattern, distinguish the parent's code running in the freshly forked child from a parent targeting another process's pidfd. Both are possible application contexts, but only the former is self-application. Do not infer that a requested service policy was applied from the launcher's identity alone.
In the source-checked peinit 0.0.12 launch path, no additional per-service mitigation mask is applied. This does not imply that a service has no mitigations: inherited flags and application self-hardening are separate. Verify the running service's committed state, including after the process is replaced; a generic launcher pattern is not evidence that peinit applies it.
Where in the process lifecycle #
A process can request additional mitigations during its life, but when it requests them changes what must be validated and what events they can constrain:
- After fork, before exec. The launcher's code running in the child can request hardening before loading the service binary. The launcher must handle a failed request rather than assume the child is hardened.
- At the binary's entry point. The program can request compatible protections early, before processing untrusted input, and check the result.
- After early-stage initialisation. This works only if the protection can be activated over the state already present. Finishing incompatible work first does not exempt its remaining mappings from validation.
The Kernel TRM specifically requires checks of existing state for runtime memory mitigations:
| Newly requested protection | Existing state that prevents activation |
|---|---|
| WXP | A writable-and-executable mapping, or another observable violation of the invariant. |
| TLP | A file-backed executable mapping with a missing or unresolvable path, a path outside the approved cache, or another TLP denial. |
| LSV | A file-backed executable mapping with missing, invalid, or insufficiently trusted signing material. |
Anonymous executable mappings are governed by WXP; TLP and LSV apply to file-backed mappings. Architecture-backed CFIF, CFIB and SML must also be made effective for the target or the request fails closed. SML can instead be satisfied when the platform reports speculation as unconditionally not affected.
Do not treat late WXP application as leaving an existing writable-executable region exempt, or late TLP/LSV application as approving libraries already loaded from disallowed paths or without acceptable signatures. The documented response is refusal of the request, not automatic repair of the process. A JIT or startup code generator needs compatibility review; there is no general “generate code first, then set WXP” recipe.
Two restrictions are event-gated: PIE governs subsequent exec, and NO_CHILD governs subsequent process creation. Set them before the event they must constrain. Neither retrospectively changes the running binary or removes children that already exist.
Fork: inheritance #
At fork, the child inherits the parent's committed mitigation flags. It cannot remove them. It receives its own process GUID, so inspect the child as a distinct process instance.
If WXP is set, it is inherited. If NO_CHILD is set, the process cannot fork a child in the first place. Creating a thread with CLONE_THREAD shares the process's PSB rather than copying it; the same committed mitigation state applies to threads in that process. See PSB Lifecycle.
Exec: preserved flags and compatibility #
Exec replaces the binary and address space, but it does not provide an escape from committed mitigations:
- Exec normally keeps the primary identity token, but
NEW_PROCESS_MINcan replace it with a lower-integrity copy when the executable has an explicit lower integrity label. Thread impersonation is reverted at exec. See Token lifecycle. - PIP fields are recomputed from the new binary's signature.
- The process security descriptor is preserved by exec; explicit descriptor or primary-token changes have their own rules.
- Mitigation flags and the process GUID are preserved.
The important exec-specific restriction is PIE. If PIE is set and the new binary is not position-independent, exec is refused with EACCES. Updating a service to a non-PIE build can therefore prevent its next start. Treat an emergency rebuild that lost PIE support as a compatibility failure to investigate, not a reason to assume the old policy has disappeared.
Preservation does not guarantee that another binary will start or work correctly under the remaining mitigations. A JIT or self-modifying program may need operations WXP refuses; executable library mappings may fail TLP or LSV checks. Do not diagnose every launch failure as PIE merely because it is the explicit exec-gated flag. Review the failing operation and the catalog.
NO_CHILD and the lifecycle interaction #
Once NO_CHILD is committed, the process cannot fork or clone a new process. A service that needs startup workers must create them before it successfully commits this restriction. Those existing workers have their own inherited state; applying NO_CHILD later to the parent does not retroactively apply it to them.
A server that forks on demand cannot use NO_CHILD during that phase. The flag still permits exec, which replaces the binary in the same process, and CLONE_THREAD, which adds a thread rather than a process. See the NO_CHILD catalog entry.
Querying mitigations #
Use the read-only logonse psb --pid PID form above, or the documented text surface /proc/<pid>/psb. A PSB report contains pip_type, pip_trust, the committed mitigations bitfield, and process_guid. It does not contain the process security descriptor or the token; those are separate inspection surfaces.
A process can read its own PSB without an access check. Reading another process's PSB needs PROCESS_QUERY_LIMITED on its descriptor, not PIP dominance. The Kernel TRM also documents SeDebugPrivilege rescuing a descriptor denial; acquiring or enabling privileges is not a required diagnostic step. A refused report is incomplete evidence, not proof that the target has no mitigations.
Readback shows what is committed, not the mask a caller merely requested. The CFI alias is not retained; NO_CHILD is part of the same bitfield and UI_ACCESS remains reserved. PIE in that report constrains subsequent exec; it does not prove that the already-running binary was checked at a prior exec. Likewise NO_CHILD does not establish that the process has never created children.
For the format, access rules and software reader, see Reading the PSB and process.h. Record the process GUID when comparing reports across restarts or correlating events, since PIDs can be reused.
What happens at process exit #
The process's PSB and its flags disappear at exit. A new process starts with what it inherits from its parent, plus whatever its launch path or its own code successfully adds. The binary's filename does not carry a saved mitigation policy.
Launchers therefore need to handle the required policy on every launch, and a self-hardening binary needs to check its own requests regardless of who launched it. Neither arrangement guarantees success simply by making a request. If a required protection is unavailable, the launch or application must treat that as a hardening failure rather than continue under a false assumption. Recheck the replacement process instead of reusing the old process's report.
Errors #
Keep the application's exact error and target context. Distinguish input and authority failures from activation failures; a valid request by an authorised caller can still fail.
| Error reported for a request | What to investigate |
|---|---|
EBADF | Invalid pidfd. |
ESRCH | The target process has exited. |
EACCES | A denied PROCESS_SET_INFORMATION check when targeting another process; inspect the reported failure rather than assuming all denials have this cause. |
EPERM | Missing PIP dominance when targeting another process. |
EINVAL | Unknown flag bits in the requested mask. |
ENODEV | The Kernel TRM specifies this unconditionally for CFIF activation against a live task. |
This is not an exhaustive activation-error catalog. Existing mapping incompatibilities, unavailable architecture support and CFIB's self-only restriction must also be considered. The SDK reference returns -1 with errno; raw kernel errors use the negative error form.
Under the documented all-or-nothing rule, a failed request commits none of that request's bits. Earlier successful commitments remain. Preserve the failure evidence, check the target's committed state, and review the required policy and supported launch path before trying a different request. Do not silently drop a required mitigation or use ALL as a recovery step.
Where to go next #
- Catalog: what each flag protects and where support or compatibility limits matter.
- logonse: existing command reference for PSB inspection and requests.
- Hardening a process: application-side startup and failure handling.
- PSB Fields: activation, current implementation limits and readback contract.
- The two-check rule: authority required for changes to another process.