# Access Decisions

---

# Access decisions

_Peios / Peios Security Fundamentals / Access Decisions_

> AccessCheck takes a token, an object's security descriptor, and a desired access mask, and runs a multi-stage pipeline to produce a granted mask.

**AccessCheck** is the function the kernel calls every time something is about to happen to a protected object. The caller has a token; the object has a security descriptor; the caller wants some set of rights. AccessCheck takes all three and produces an answer: which of those rights are granted, which are not, and whether to fire any audit events as a result.

The function is not a single comparison. It is a pipeline of layers, each of which can constrain the result. A right that the DACL would have granted can be removed by a later layer (restricted-token intersection, confinement, central access policy). A right that the DACL would not have granted can be added by an earlier layer (privilege grants). Following an access decision through is following each layer in order.

This page walks the pipeline from start to finish, names every stage, and points each one at the topic that covers it in depth.

## The shape of an access check

```mermaid
flowchart LR
    A["Token (caller)"] --> X["AccessCheck"]
    B["Security descriptor (object)"] --> X
    C["Desired mask (caller asked for)"] --> X
    D["Optional inputs: privilege intent, self SID, local claims, object type list, PIP from PSB"] --> X
    X --> R["Granted mask + audit emissions + continuous audit mask + staging mismatch flag"]
```

Inputs:

| Input | Source | What it contributes |
|---|---|---|
| Token | The calling thread's effective token. | Identity (user SID, groups, restricted SIDs), token-level state (integrity level, mandatory policy, privileges, confinement). |
| Security descriptor | The object being accessed. | Owner, primary group, DACL, SACL — everything the policy on the object says. |
| Desired mask | The caller. | The 32-bit access mask of rights the caller wants. May include `MAXIMUM_ALLOWED` to ask "what could I have?". |
| `privilege_intent` | The caller. | Flags for backup and restore intent — see [Intent-gated privileges](/peios/security-fundamentals/privileges/intent-gated.md). |
| `self_sid` | The caller. | The SID that `PRINCIPAL_SELF` (`S-1-5-10`) should resolve to during the DACL walk. Used by directory-style objects. |
| `local_claims` | The caller. | Per-call attributes in the `@Local.*` namespace, available to conditional ACE expressions. |
| Object type list | The caller (optional). | A tree of property GUIDs for per-property evaluation. See "Object type list" below. |
| PIP type and trust | The PSB of the calling process. | Used by the PIP step to compare against any trust label on the object. |
| Object audit context | The caller. | An opaque blob included in any audit events emitted for this call, identifying the object to audit consumers. |

Outputs:

| Output | Meaning |
|---|---|
| Granted mask | The 32-bit access mask of rights the caller actually gets. |
| Audit emissions | Any audit events that fired (SACL audit ACEs, privilege-use, token-forced policy events). |
| Continuous audit mask | For file-like objects, the per-operation audit mask to cache on the open handle. See [The SACL](/peios/security-fundamentals/security-descriptors/the-sacl.md). |
| Staging mismatch flag | Set when a staged CAAP policy would have produced a different result. See [Central access policies](/peios/security-fundamentals/central-access-policies/overview.md). |

The function does not return a boolean. It returns a granted mask, and the caller is responsible for comparing that mask against what was requested. "Access denied" is what happens when the granted mask does not contain all the requested rights — but partial grants are visible to the caller, and `MAXIMUM_ALLOWED` callers specifically want to know everything they would have gotten.

## The pipeline, step by step

The pipeline runs in a defined order. Each step either pre-decides bits, modifies the running state, or both. The kernel implements it as a 15-step procedure; this section names each step, says what it does, and points to where it is covered in detail.

| Step | Name | What it does |
|---|---|---|
| 0 | **Impersonation level gate** | If the effective token is Impersonation-type at the Identification level, deny everything immediately. The token may be inspected but not used for access decisions. |
| 1 | **Input validation** | The SD must be present, must have an owner, must have a primary group. Object type list must be well-formed. Malformed inputs are rejected with appropriate errors. |
| 2 | **Generic mapping** | Generic rights in the desired mask (`GENERIC_READ`, `GENERIC_WRITE`, etc.) are expanded to object-specific bits using the object's GenericMapping table. `MAXIMUM_ALLOWED` is stripped and remembered as a mode. |
| 3 | **Effective privileges** | Backup and restore privileges are stripped from the privilege set unless the caller passed the corresponding intent flag. See [Intent-gated privileges](/peios/security-fundamentals/privileges/intent-gated.md). |
| 4 | **Privilege grants** | Pre-decide bits that are granted by privilege regardless of the DACL: `ACCESS_SYSTEM_SECURITY` via SeSecurity, backup-read bits via SeBackup, write/metadata bits via SeRestore. See [Privileges in the pipeline](/peios/security-fundamentals/access-decisions/privileges-in-the-pipeline.md). |
| 5 | **Pre-SACL walk** | Scan the SACL for the mandatory integrity label (MIC) and the PIP trust label. Pre-decide write/read/execute bits as denied for non-dominant callers. Resource attributes are extracted and indexed for conditional expressions. See [Mandatory integrity control](/peios/security-fundamentals/access-decisions/mandatory-integrity-control.md) and [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md). |
| 6 | **Virtual group injection** | `OWNER RIGHTS` (`S-1-3-4`) and `PRINCIPAL_SELF` (`S-1-5-10`) are injected into the token's group view if the caller owns the object or matches `self_sid` respectively. See [Ownership](/peios/security-fundamentals/security-descriptors/ownership.md). |
| 7 | **Tree initialisation** | If the caller passed an object type list (per-property evaluation), the pipeline's `decided`/`granted` state is seeded per node. Skipped for ordinary calls. |
| 8 | **Normal DACL evaluation** | Grant owner implicit rights (READ_CONTROL, WRITE_DAC) unless suppressed by OWNER RIGHTS. Walk the DACL using first-writer-wins, applying ACE flags, propagating per-property results for object type lists. See [DACL evaluation](/peios/security-fundamentals/security-descriptors/dacl-evaluation.md). |
| 9 | **Post-DACL SeTakeOwnership** | If the walk did not grant `WRITE_OWNER` and the mandatory policy did not block it, `SeTakeOwnershipPrivilege` grants it now. See [Ownership](/peios/security-fundamentals/security-descriptors/ownership.md). |
| 10 | **Restricted token pass** | If the token is restricted, the DACL is re-evaluated using only the restricted SIDs, and the result is intersected with step 8's result. Privilege-granted bits from step 4 are restored after the intersection. See [Restricted and write-restricted tokens](/peios/security-fundamentals/tokens/restricted-tokens.md). |
| 11 | **Confinement pass** | If the token is confined, the DACL is re-evaluated against the confinement SID set and the result is absolutely intersected. Privileges do **not** bypass confinement. See [Confinement](/peios/security-fundamentals/confinement/overview.md). |
| 12 | **CAAP evaluation** | Each `SYSTEM_SCOPED_POLICY_ID` ACE in the object's SACL is resolved and evaluated. Each rule's DACL is intersected with the running grant. Staging mismatches are recorded. See [Central access policies](/peios/security-fundamentals/central-access-policies/overview.md). |
| 13 | **Privilege-use audit emission** | For each privilege that contributed bits, an audit event is emitted if the token's `audit_policy` requested it (success or failure variant depending on whether the bits survived). |
| 14 | **SACL audit walk** | Walk the object's SACL and all collected CAAP effective SACLs for `SYSTEM_AUDIT*` and `SYSTEM_ALARM*` ACEs. Emit audit events; compute continuous-audit mask. See [Auditing](/peios/security-fundamentals/auditing/overview.md). |
| 14b | **Token audit policy** | The token's `audit_policy` field can force audit events independently of any SACL ACE. Success and failure flags emit policy-forced events. |
| 15 | **Result computation** | The final granted mask is `granted & mapped_desired`. The function returns this mask, plus the audit emissions, the continuous-audit mask, and the staging mismatch flag. |

The numbering is the kernel's; you will see it in audit and error messages.

## Reading the pipeline

A few observations about the shape that are worth pinning before reading the deeper pages:

**Pre-DACL layers can pre-decide bits as denied.** MIC and PIP do this for non-dominant callers. A bit pre-decided as denied stays denied; no later step can grant it. The owner of an object that has a high mandatory label cannot read it from a lower-integrity session even though the DACL would grant the read.

**Pre-DACL layers can pre-decide bits as granted.** Step 4 (privilege grants) does this. Bits granted by privilege are decided before the DACL walk runs; the walk's first-writer-wins applies to undecided bits, so a privilege grant is effectively immune to a later DACL deny.

**The DACL walk is the central act.** Steps 0–7 set up state for it; steps 9–12 narrow what it produced. The walk itself, governed by [DACL evaluation](/peios/security-fundamentals/security-descriptors/dacl-evaluation.md), is where most of the visible policy lives.

**Narrowing layers (restricted, confinement, CAAP) only remove access, never add.** A right that the DACL plus privileges granted may be lost in steps 10–12. A right that those did not grant cannot be added by the narrowing layers.

**Audit fires last and observes everything.** By the time the audit walk runs (step 14), the granted mask is final. Audit ACEs can compare against the requested mask, the granted mask, and the difference, and decide whether to emit. Token-forced audit policy can emit even when no SACL ACE matched.

**A staging mismatch is informational, not corrective.** Step 12 evaluates both the effective CAAP rules (which affect the granted mask) and the staged CAAP rules (which do not). If they would have produced different results, the function reports it; it does not change the granted mask.

## Two AccessCheck variants

The kernel exposes two AccessCheck syscalls. They differ in what they return:

| Syscall | What it does |
|---|---|
| **`kacs_access_check`** | The common case. Returns a single granted mask for the whole object. |
| **`kacs_access_check_list`** | The per-property variant. Requires an object type list and returns a separate granted mask and status per node in the tree. Used by directory-style objects with property-level access control. |

The pipeline is the same for both. The list variant just tracks per-node state through every step, so an object type list with twelve nodes produces twelve granted masks at the end, one per node.

For most objects (files, registry keys, tokens), the regular variant is the right one. Per-property is meaningful only when the object has properties that can be granted independently — for example a directory object with named attributes.

## Object type list, briefly

Object ACEs in a DACL carry a property GUID. When the caller passes an object type list, AccessCheck evaluates each ACE against each node of the tree, propagating per-property grants and denials according to the tree structure (a grant on a parent set flows to its children; a denial flows up; sibling grants intersect).

The tree itself is provided by the caller as a preorder-flat array of `kacs_object_type_entry` records. The array must be well-formed: starts at level 0, no level gaps, no duplicate GUIDs, and so on. Malformed lists are rejected at step 1.

The full mechanics of object ACEs and the object type list are in [ACLs, ACEs, and access masks](/peios/security-fundamentals/security-descriptors/acls-and-aces.md). Most code never builds an object type list — they exist for the directory-object case specifically.

## Where to start

If you want MIC in depth — what an integrity level actually does to access, how the policy bits work, how the SD's mandatory label drives it — read [Mandatory integrity control](/peios/security-fundamentals/access-decisions/mandatory-integrity-control.md).

If you want to know exactly which bits each AccessCheck-influencing privilege grants and where in the pipeline that grant happens, read [Privileges in the pipeline](/peios/security-fundamentals/access-decisions/privileges-in-the-pipeline.md).

If you want the narrowing layers — restricted-token pass, confinement pass, CAAP — in detail, read [Narrowing layers](/peios/security-fundamentals/access-decisions/narrowing-layers.md).

If you are debugging an unexpected denial right now, read [Debugging a denial](/peios/security-fundamentals/access-decisions/debugging-a-denial.md).

---

# Mandatory integrity control

_Peios / Peios Security Fundamentals / Access Decisions_

> MIC gates access by integrity level — the object's label in its SACL against the token's own level, compared before the DACL walk ever runs.

**Mandatory integrity control** is the layer of the access check that gates rights based on a numeric trust axis called the **integrity level**. Where the DACL asks "is this principal allowed?", MIC asks "is the caller trusted enough to do this regardless of the DACL?". A non-dominant caller — one whose integrity is lower than the object's — has certain rights pre-decided as denied, before the DACL is even looked at.

The integrity axis is independent of identity. Two threads, same user, can run at different integrity levels and see different effective access. An interactive shell at Medium and an elevated session at High are the same person, separated by integrity. MIC enforces that separation.

MIC fires in step 5 of the access check pipeline — pre-DACL. Its decisions are immutable from that point. The DACL walk, the restricted-token pass, none of them can undo a MIC denial.

## The integrity levels

In practice there are five standard integrity levels, strictly ordered — treat integrity as an enum even though, technically, it is not one (see below). These five are the levels anyone normally works with (canonical value catalog: [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md)):

| Level | SID | RID | Typical use |
|---|---|---|---|
| **Untrusted** | `S-1-16-0` | 0 | Tokens for highly-sandboxed processes. The lowest level. |
| **Low** | `S-1-16-4096` | 4096 | Tokens for confined applications, anti-malware quarantines. |
| **Medium** | `S-1-16-8192` | 8192 | The default for interactive sessions and most services. |
| **High** | `S-1-16-12288` | 12288 | Elevated administrative sessions. The Full token in a UAC-style linked pair. |
| **System** | `S-1-16-16384` | 16384 | The kernel, peinit, authd, and the rest of the TCB. |

The comparison is the obvious one: Untrusted < Low < Medium < High < System. A caller **dominates** an object if the caller's integrity level is at least the object's level. Otherwise the caller is non-dominant and MIC's policy bits decide what they cannot do.

Technically, though, the level is not an enum. An integrity level is simply the single sub-authority of an `S-1-16` mandatory-label SID, read as an unsigned integer, and levels are compared numerically. Any `S-1-16-<n>` SID with exactly one sub-authority is a valid level; only a SID with the wrong identifier authority (not `S-1-16`) or more than one sub-authority is malformed and rejected. The kernel does not check a label SID against a hardcoded list — an unusual value is interpreted purely by its numeric position, and the five standard RIDs are spaced by 4096 to leave room between them. This mostly matters for Windows interop, where non-standard levels appear: `medium-plus` (`S-1-16-8448`) sits just above Medium, and `protected` (`S-1-16-20480`) sits above System. Treat the five named levels as the working model; just don't assume they are the only values a label can carry.

## Where the object's label lives

An object's mandatory integrity label is in its SACL, as a `SYSTEM_MANDATORY_LABEL_ACE`. The ACE has:

- A **SID** from the integrity namespace (an `S-1-16` SID with one sub-authority — normally one of the five above, occasionally a non-standard level such as `medium-plus` or `protected`).
- A **mask** containing the MIC policy bits.

When the access check looks at an SD, it scans the SACL for `SYSTEM_MANDATORY_LABEL_ACE` entries that are not inherit-only. The first such ACE is the object's effective label. If there is none, the object's effective label is **Medium with `NO_WRITE_UP`** — that is the default for an unlabelled object.

The Medium-with-NO_WRITE_UP default exists to protect default objects from lower-integrity callers without requiring every object to carry an explicit label. An unlabelled file on a standard filesystem is automatically protected against writes from a Low-integrity sandboxed process; the sandbox cannot scribble on system data even though no one explicitly marked the data with a label.

The SACL ACE also lives there for a reason: changing the integrity label is a privileged operation (gated by `SeRelabelPrivilege` for label-raising and by the standard SACL access rule for label-lowering). Stored as a SACL ACE, the label is automatically under SACL access control — the owner of an object cannot lower its integrity label without `SeSecurityPrivilege` or `SeRestorePrivilege`. This is exactly the right gate; a user should not be able to weaken the protections on their object just because they own it.

## The MIC policy bits

The mask of a `SYSTEM_MANDATORY_LABEL_ACE` contains MIC policy bits that say what non-dominant callers cannot do (canonical values: [ACE types and flags](/peios/using-peios/constants-and-catalogs/ace-types-and-flags.md)):

| Bit | Value | Effect on non-dominant callers |
|---|---|---|
| `SYSTEM_MANDATORY_LABEL_NO_READ_UP` | 0x01 | Read-category rights are denied. |
| `SYSTEM_MANDATORY_LABEL_NO_WRITE_UP` | 0x02 | Write-category rights are denied. |
| `SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP` | 0x04 | Execute-category rights are denied. |

The names use "up" because the non-dominant caller is *below* the object. A "no write up" rule denies a Medium caller from writing to a High object — the caller would be writing "up" to higher integrity.

The bits compose. A label with all three set blocks all access categories from non-dominant callers. A label with only `NO_WRITE_UP` allows non-dominant callers to read but not write. The most common combination by far is `NO_WRITE_UP` alone — it is the default, and it captures the most important integrity rule (lower-integrity code should not be able to modify higher-integrity data).

Read-up and execute-up restrictions are less common. They appear on objects whose existence or contents should be hidden from lower-integrity callers entirely.

## How MIC fires in the access check

```mermaid
flowchart LR
    A["Token integrity level"] --> M["MIC compare"]
    B["Object integrity label (or Medium/NWU default)"] --> M
    M -->|caller dominates: pass| C["Continue to DACL walk"]
    M -->|caller does not dominate: apply policy bits| D["Pre-decide affected bits as denied"]
    D --> C
```

At step 5 of the pipeline:

1. Extract the object's integrity label (or apply the Medium-with-NO_WRITE_UP default if there is no label ACE).
2. Compare the token's `integrity_level` against the object's label.
3. If the token's level is at least the object's level, MIC is satisfied. Nothing happens; the pipeline proceeds.
4. If the token's level is below the object's, look at the label's policy bits. For each bit set, take the corresponding category of rights from the desired mask and mark those bits as decided-and-denied. The bits enter the pipeline's `decided` set but not `granted`.

The categories — read, write, execute — are defined by the object's GenericMapping. For a file, the read category is the set of bits that map from `GENERIC_READ` (`FILE_READ_DATA`, `FILE_READ_ATTRIBUTES`, `FILE_READ_EA`, `READ_CONTROL`, `SYNCHRONIZE`). Same idea for the write and execute categories.

By the time the pipeline reaches the DACL walk in step 8, the bits MIC denied are already decided. The walk processes the remaining bits; the denied ones are skipped.

## The "token mandatory policy" field

A separate but related field: every token carries a `mandatory_policy` bitmask. It uses two flags:

| Flag | Value | Effect |
|---|---|---|
| `NO_WRITE_UP` | 0x01 | The MIC rule is active for this token's accesses. |
| `NEW_PROCESS_MIN` | 0x02 | At exec, if the executable's integrity label is lower than the token's, the token's integrity is lowered to match. |

Both flags are set at token creation and **cannot be changed at runtime**. A process cannot relax its own MIC policy — the kernel rejects any AdjustPrivileges-style call that would. The reasoning: a Medium-integrity process should not be able to silently turn off its own write-up restriction.

`NO_WRITE_UP` is set on essentially every token authd issues. It is the active state of MIC enforcement, and it is worth understanding that it is load-bearing rather than a formality: a token without it carries an integrity level that nothing consults. The access check returns before comparing labels at all, so such a token holds a level that is pure decoration — it neither restricts the token nor protects anything from it.

## Where a token's level comes from

The object's label comes from its SACL, as above. The **token's** level is decided once, when the token is minted, and never changes afterwards.

For a logon token that is **local policy** — a per-principal record in the registry that `authd` reads at every sign-in, keyed on the SIDs the token ends up carrying. A principal source has no way to influence it, deliberately: how much a machine trusts someone is not a fact about them that a directory could know. A principal no record names gets Medium. See [assigning privileges](/peios/security-fundamentals/privileges/assigning-privileges.md) for the record format.

For service tokens, peinit decides. For the SYSTEM token, the kernel does.

`NEW_PROCESS_MIN` is set on tokens that want exec to enforce integrity boundaries. When set, exec of a binary marked at a lower level produces a token whose integrity is lowered. This is what prevents a High-integrity shell from launching a Medium-labelled binary as High-integrity — the resulting process is Medium, regardless of who launched it.

## What MIC does not constrain

A surprisingly important set of things.

**MIC does not constrain privilege grants.** Step 4 of the pipeline (privilege grants) runs *before* MIC, and the bits it pre-decides as granted survive MIC. A caller holding `SeBackup` with `BACKUP_INTENT` set gets read-category rights via the privilege regardless of MIC. The reason: privileges are explicit, intentional, and audited — they are not subject to the same "block by default" treatment as the DACL.

The exception is **PIP**. Where MIC ignores privilege grants, PIP strips them. A non-dominant PIP caller loses privilege-granted rights, not just DACL-granted ones. PIP is the stricter integrity-style enforcement; MIC is the looser one. See [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md).

**MIC does deny WRITE_OWNER sought via SeTakeOwnership.** Unlike the step-4 privileges, SeTakeOwnership fires post-DACL (step 9) and only on bits not already mandatorily decided — and MIC pre-decides WRITE_OWNER as denied for every non-dominant caller. A tool that needs ownership of higher-integrity objects must hold `SeRelabelPrivilege` (the one privilege that punches WRITE_OWNER through MIC) or use `SeRestorePrivilege` with `RESTORE_INTENT`, whose step-4 grant precedes MIC.

**MIC does not lower integrity automatically on access denial.** A non-dominant caller blocked by MIC just gets the relevant bits denied. The token's integrity is not adjusted. The same token can succeed at accessing a lower-integrity object on the next call.

**MIC is asymmetric.** A non-dominant caller is constrained; a dominant caller is not. There is no "no write down" — a High-integrity process writing to a Medium object faces no MIC restriction. This sometimes surprises people. The model is "lower integrity cannot touch higher", not "integrity levels cannot interact at all".

## Integrity and impersonation

The two-gate model in [Impersonation](/peios/security-fundamentals/impersonation/the-two-gates.md) refuses to let a server act as a client of higher integrity. A Medium-integrity service that captures a High-integrity client's peer token gets an **Identification-level** impersonation token — it can inspect the identity, but no access check will pass.

This is enforced unconditionally. `SeImpersonatePrivilege` does not bypass it. A High-integrity client connecting to a Medium service should not be able to make the Medium service operate at High — that would void the MIC model for the service.

The implication for MIC enforcement: a non-dominant caller cannot escape MIC by impersonating someone with higher integrity. The integrity ceiling downgrades such a token to Identification, so it can never be used to act at a higher integrity than the server's own.

## Setting an integrity label

The label on an object is changed via `kacs_set_sd` with `LABEL_SECURITY_INFORMATION` (or `SACL_SECURITY_INFORMATION` for the whole SACL). The rules:

- **Lowering a label** (to a level at or below the caller's own integrity) is permitted. No special privilege required beyond the normal SACL access rule (`SeSecurityPrivilege` or `SeRestorePrivilege`).
- **Raising a label** (to a level above the caller's own integrity) requires `SeRelabelPrivilege`. The privilege is rare; only the TCB and specific labelling services hold it.

The asymmetry is the obvious one: you can always demote an object to your own level or lower, but you cannot promote an object beyond what you yourself can reach. This prevents a Medium administrator from labelling an object High and then locking themselves out from below — they cannot label objects at a level they could not access.

For `LABEL_SECURITY_INFORMATION` specifically, the call is mutually exclusive with `SACL_SECURITY_INFORMATION` in the same `kacs_set_sd` invocation. You either update the integrity label only, or you update the whole SACL.

## MIC vs PIP in one paragraph

MIC and PIP look superficially similar. Both compare a level on the caller against a level on the object. Both block non-dominant access. The differences are crucial:

- **MIC uses the effective token's `integrity_level`. PIP uses the PSB's `pip_type` and `pip_trust`.** Impersonation changes the effective token but not the PSB; MIC respects impersonation, PIP does not.
- **MIC is a one-axis comparison. PIP is two axes (type and trust), both of which must dominate.**
- **MIC ignores privilege grants. PIP strips them.** A privilege can rescue access from MIC; it cannot rescue access from PIP.
- **MIC has a default (Medium/NO_WRITE_UP). PIP has no default.** Objects without a `SYSTEM_PROCESS_TRUST_LABEL_ACE` are not PIP-protected; objects without a `SYSTEM_MANDATORY_LABEL_ACE` get the default MIC treatment.

The two layers complement each other. MIC is the "user identity is at this trust level" axis; PIP is the "the binary running this process has this trust level" axis. An access can be blocked by either, allowed by both, or — in the rare cases where a privilege bridges MIC but not PIP — allowed by MIC and blocked by PIP.

## Where to go next

For the privileges that can carry an access past MIC — and where each fires in the pipeline — read [Privileges in the pipeline](/peios/security-fundamentals/access-decisions/privileges-in-the-pipeline.md).

For the binary-trust axis MIC is so often compared with, read [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md).

---

# Privileges in the pipeline

_Peios / Peios Security Fundamentals / Access Decisions_

> Where each AccessCheck-influencing privilege fires in the pipeline, which bits it grants, what it does not bypass, and how its grant is recorded for audit.

Five privileges in the catalog change what AccessCheck decides. Each one fires at a specific step of the pipeline. Each one grants a specific set of bits. Each one is bypassed by some layers and not by others. Understanding which is which is what lets you reason about access decisions that involve privileges — when a privilege rescued an access, when it could have but did not, when it would have been irrelevant.

This page covers each of the five AccessCheck-influencing privileges in pipeline order: where it fires, what it grants, what bypasses it, and how it shows up in audit.

The intent model itself — why `BACKUP_INTENT` and `RESTORE_INTENT` exist as separate gates — is on [Intent-gated privileges](/peios/security-fundamentals/privileges/intent-gated.md). This page assumes that material.

## The five privileges, at a glance

| Privilege | Fires at step | Grants | Intent-gated? |
|---|---|---|---|
| `SeSecurityPrivilege` | 4 | `ACCESS_SYSTEM_SECURITY` | No |
| `SeBackupPrivilege` | 4 | Read-category rights | Yes (`BACKUP_INTENT`) |
| `SeRestorePrivilege` | 4 | Write-category rights, `WRITE_OWNER`, `WRITE_DAC`, `DELETE`, `ACCESS_SYSTEM_SECURITY` | Yes (`RESTORE_INTENT`) |
| `SeTakeOwnershipPrivilege` | 9 | `WRITE_OWNER` | No |
| `SeRelabelPrivilege` | (not AccessCheck — `kacs_set_sd`) | Setting an integrity label above the caller's own | No |

Steps 4 and 9 are both privilege-grant steps in the access check pipeline (see [Access decisions overview](/peios/security-fundamentals/access-decisions/overview.md)). The difference is when in the pipeline they run. Step 4 is pre-DACL — privileges that fire there decide bits before the DACL walk happens. Step 9 is post-DACL — `SeTakeOwnershipPrivilege` fires only if the DACL did not already grant `WRITE_OWNER`.

`SeRelabelPrivilege` does not fire inside AccessCheck at all. It is consulted by `kacs_set_sd` when the caller is trying to change an object's integrity label above their own. It is listed here because, like the others, it modifies what would otherwise be a denial decision.

## SeSecurityPrivilege — SACL access

`ACCESS_SYSTEM_SECURITY` (bit 0x01000000) gates reading and writing the SACL. The DACL **never** grants this right — there is no ACE you can write to allow non-privileged users SACL access. The only grant of `ACCESS_SYSTEM_SECURITY` comes from a privilege.

`SeSecurityPrivilege`, when enabled on the calling token, grants `ACCESS_SYSTEM_SECURITY` at step 4 of the pipeline if it appears in the desired mask. The grant is unconditional in the sense that no DACL or MIC bit can deny it. PIP can: a non-dominant PIP caller will lose this bit along with the other privilege-granted bits.

In addition to its access-check role, `SeSecurityPrivilege` is also a kernel-standalone privilege that gates several Linux capability checks (CAP_AUDIT_CONTROL, CAP_MAC_ADMIN, CAP_AUDIT_READ). The same privilege wears two hats.

`SeRestorePrivilege` also grants `ACCESS_SYSTEM_SECURITY` in its specific use cases (restoring an SD via `kacs_set_sd`). The two privileges are not equivalent: a holder of SeSecurity can read or write any SACL freely; a holder of SeRestore can do it as part of a restore operation but the rest of the access-check pipeline treats them differently.

In audit, an `ACCESS_SYSTEM_SECURITY` grant attributable to `SeSecurityPrivilege` is recorded as such. A consumer parsing audit events can distinguish "SACL was modified because the DACL granted it" (cannot happen) from "SACL was modified because the caller held the privilege" (always the cause).

## SeBackupPrivilege — read-category, intent-gated

`SeBackupPrivilege` is the read-bypass privilege. With `BACKUP_INTENT` set in `privilege_intent` and the privilege enabled on the token, AccessCheck at step 4 grants the **read-category bits** of the desired mask regardless of the DACL.

The "read category" is determined by the object's GenericMapping. For a file: `FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | READ_CONTROL`. For a registry key: the corresponding read rights on a key. For a token: `TOKEN_QUERY | READ_CONTROL`.

The grant is pre-decided in step 4. By the time the DACL walk runs in step 8, the read-category bits are already decided as granted, and the walk only processes the remaining (write, execute, ownership) bits. If the DACL would have granted some read bits anyway, the privilege grant is redundant; if the DACL would have granted none, the privilege grant is the source.

The intent flag is what activates the privilege. Without `BACKUP_INTENT`, step 4's SeBackup processing is skipped; the privilege has no effect on this call. The intent must come from the caller — there is no kernel default. See [Intent-gated privileges](/peios/security-fundamentals/privileges/intent-gated.md).

## SeRestorePrivilege — write-category, intent-gated

`SeRestorePrivilege` is the write-bypass privilege. With `RESTORE_INTENT` set and the privilege enabled, AccessCheck at step 4 grants the **write-category bits** plus several adjacent rights:

- Write-category bits (`FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA` for files; corresponding bits for other types).
- `DELETE`.
- `WRITE_OWNER` and `WRITE_DAC`.
- `ACCESS_SYSTEM_SECURITY` (so a restore can rewrite the SACL).

That last bit is the reason a restore tool does not also need to hold SeSecurityPrivilege — SeRestore folds the SACL-write authority into the restore-flavoured grant.

Beyond the access-check grant, SeRestore has one more effect: it bypasses the "new owner must be self or `SE_GROUP_OWNER` group" restriction in `kacs_set_sd`. A holder of SeRestore can set an object's owner to any well-formed SID, not just their own. This is what lets a restore reconstitute an object's original owner even when the original principal is not present on the running system.

Like SeBackup, the privilege is intent-gated. Without `RESTORE_INTENT`, it is skipped at step 4 and has no effect.

## SeTakeOwnershipPrivilege — WRITE_OWNER, post-DACL

`SeTakeOwnershipPrivilege` is special because of when it fires: step 9, after the DACL walk. The reasoning: the DACL might already grant `WRITE_OWNER` to the caller, in which case the privilege is unnecessary. The pipeline runs the walk first, then checks: did the walk grant `WRITE_OWNER`? If yes, nothing to do. If no, and the caller holds SeTakeOwnership, grant it now.

This timing means SeTakeOwnership produces a different audit signature than the step-4 privileges. The "this right was granted by SeTakeOwnership" event fires only when the DACL would not have granted `WRITE_OWNER` anyway. If you see SeTakeOwnership in an audit log, you know the caller actually needed the privilege for that access; if the DACL had been sufficient, the privilege would not have shown up.

SeTakeOwnership does not bypass MIC. A Medium-integrity caller cannot take ownership of a High-integrity object even with the privilege, because MIC will have pre-decided `WRITE_OWNER` as denied at step 5 (before SeTakeOwnership runs at step 9). The privilege only fires on bits not already decided.

It also does not bypass PIP — same reasoning, PIP fires before step 9 and can pre-decide `WRITE_OWNER` as denied for non-dominant PIP callers.

The "new owner must be self or `SE_GROUP_OWNER` group" rule applies to taking ownership via this privilege. The privilege grants the right to write the owner field, not freedom from validation. To set the owner to an arbitrary SID, the caller additionally needs SeRestore.

## SeRelabelPrivilege — raising the integrity label

`SeRelabelPrivilege` is the integrity-label privilege. It does two things:

- During `kacs_set_sd` with `LABEL_SECURITY_INFORMATION` (or `SACL_SECURITY_INFORMATION`), it allows the caller to set the integrity label to a level **above their own**. Without the privilege, an object's label can be lowered (to at or below the caller's level) but not raised.
- It bypasses the standard MIC check on `WRITE_OWNER` when the caller is operating on an object with a higher integrity label — specifically, the privilege "punches `WRITE_OWNER` through MIC" for non-dominant callers, so a relabelling tool can adjust ownership as part of a relabel even if the object is currently at a higher integrity than the tool.

`SeRelabelPrivilege` is one of the rarest privileges. It is held only by tools that legitimately need to set system-level integrity labels — typically TCB labelling utilities. Almost no general-purpose code holds it.

## What privileges do not bypass

A handful of layers ignore the privilege-grant step entirely.

**Confinement.** Confinement pass (step 11) intersects the running grant with what the confinement SID set would grant. Privilege-granted bits are **not** preserved through this intersection. A confined token with SeBackup enabled can still be confined out of read access; the privilege is gone.

This is the major difference between confinement and the restricted-token pass. Restricted-token intersection (step 10) preserves privilege-granted bits — they are restored after the intersection. Confinement does not. The reason: confinement is policy applied to the code from outside, and a confined application should not be able to escape its sandbox by exercising a privilege. See [Confinement](/peios/security-fundamentals/confinement/overview.md).

**CAAP.** Central access policies, evaluated at step 12, can narrow what privileges have already granted. A CAAP rule whose DACL does not grant the right will strip it from the running grant. Privilege-granted bits are not immune.

**PIP.** Non-dominant PIP callers have their privilege-granted bits stripped in step 5 (PIP processing). This is unique to PIP — MIC is content to leave privilege-granted bits alone, but PIP actively revokes them. The reason is the same as the confinement reason: PIP is about isolating untrusted binaries from trusted ones, and a privilege that bypassed PIP would defeat the model.

A useful mnemonic: privileges survive restricted-token narrowing; they do not survive confinement, CAAP, or PIP. The first is the privilege-friendly narrowing layer; the others are privilege-blind.

## Privilege-use audit

Step 13 of the pipeline is the privilege-use audit emission. For each privilege that contributed bits in steps 4 or 9, the kernel checks the token's `audit_policy` and emits an audit event accordingly.

The events fall into two flavours:

- **Successful privilege use.** The privilege contributed bits and those bits survived to the final granted mask. Emitted when `PRIVILEGE_USE_SUCCESS` (0x04) is set in `audit_policy`.
- **Failed privilege use.** The privilege contributed bits but they did not survive — stripped by some later layer (confinement, CAAP, PIP). Emitted when `PRIVILEGE_USE_FAILURE` (0x08) is set.

The failed-use case is what tells you "a privilege was exercised but was unable to actually grant access". This is auditable separately from a plain "access denied" — the privilege got far enough to fire and then got stripped.

The events themselves are documented in [Auditing](/peios/security-fundamentals/auditing/overview.md).

## Reading a granted mask back

A practical concern: when an access check returns a granted mask that includes a right, how do you know whether the right came from the DACL or from a privilege?

You cannot tell from the granted mask alone. The bit is the same regardless of source. The way to tell is the audit log: privilege-use audit events identify which privilege contributed which bits. If you see in the audit log that SeBackup granted `FILE_READ_DATA` on a specific access, you know the DACL did not.

In code, the way to verify is to issue the access check twice: once with the privilege intent flag (or with the privilege enabled), and once without. The difference in granted mask is what the privilege contributed.

For most code paths, this introspection is unnecessary — the granted mask is whatever it is, and the caller acts on it. But for auditing, debugging, or testing pipelines that exercise privileged paths, the distinction matters.

## What goes in audit

The privilege-use audit event for each fire records:

- The name of the privilege (e.g. `SeBackupPrivilege`).
- The bits the privilege contributed pre-narrowing.
- The bits that survived to the final granted mask.
- Whether the use was successful (bits survived) or failed (bits were stripped).

This is enough for an audit consumer to reconstruct what happened. A success event with non-empty surviving bits shows what the privilege ended up granting; a failure event with empty surviving bits shows what the privilege tried to grant and lost. Together they tell the story of where the access landed.

## Where to go next

For the three layers that can strip privilege-granted bits after the fact, read [Narrowing layers](/peios/security-fundamentals/access-decisions/narrowing-layers.md).

For the privilege-use events themselves and where they end up, read [Auditing](/peios/security-fundamentals/auditing/overview.md).

---

# Narrowing layers

_Peios / Peios Security Fundamentals / Access Decisions_

> The three layers that can only remove access — the restricted-token pass, confinement, and central access policies — in pipeline order, and what bypasses each.

The access check has three layers that **narrow** the running granted mask: the restricted-token pass at step 10, the confinement pass at step 11, and the central access policy evaluation at step 12. Each runs after the DACL walk has produced an initial grant. Each is a strict intersection — they can only remove bits from the grant, never add. A right granted by some earlier layer (the DACL or a privilege) may be lost in these passes. A right not granted by an earlier layer cannot be created by them.

This page covers the three layers in pipeline order, the rules for what does and does not bypass each one, and how they compose when more than one is active.

## The order, and why it matters

| Step | Layer | What it intersects against |
|---|---|---|
| 10 | Restricted-token pass | A DACL walk using only the token's `restricted_sids` |
| 11 | Confinement pass | A DACL walk using only the token's confinement SID set |
| 12 | CAAP evaluation | The effective DACLs of each referenced central access policy |

The order is fixed. The restricted pass runs first because it is identity-flavoured — the kernel is asking "would the restricted SIDs alone have been granted this?". Confinement runs next because it is policy applied to the code as a whole, and the inputs to the confinement check are bigger than just the SIDs. CAAP runs last because it can layer multiple policies on top of everything.

A key consequence of the order: a right that survives all three is granted. A right denied by any one is denied. Composition is conjunction — every active narrowing layer must permit the right for it to remain in the final granted mask.

## Restricted-token pass

If the token is not restricted (its `restricted_sids` list is empty), this step is skipped entirely. If it is, the kernel runs the DACL walk a second time, using only `restricted_sids` as the matching set. The `user_sid` does not participate. The `groups` list does not participate. Only entries in `restricted_sids` count.

The result is `granted_restricted` — what the DACL would grant if the token had only its restricted SIDs. This mask is intersected with the running grant from step 8: `granted = granted & granted_restricted`.

**Privileges bypass this pass.** After the intersection, bits that were granted by privileges in steps 4 or 9 are **restored** to the running grant. The reason is that privileges are explicit and authorised independently of identity-based narrowing — a restricted token holder with `SeBackup` enabled and `BACKUP_INTENT` set can still exercise the privilege.

**The write-restricted variant** narrows only write-category bits. Reads and execute come from the step-8 grant alone. The bits in the write category are determined by the object's GenericMapping — for files, that is whatever GENERIC_WRITE maps to. See [Restricted and write-restricted tokens](/peios/security-fundamentals/tokens/restricted-tokens.md) for the full mechanics, including the `user_deny_only` flag that write-restricted tokens carry.

The restricted-token pass is **the program's narrowing layer**. It is set up in code: a sandbox launcher FilterTokens its own token down to a restricted version before launching the constrained work. The code is choosing to give itself less authority. Privileges, being explicit, are not subject to this self-imposed narrowing.

## Confinement pass

If `confinement_sid` is null on the token, this step is skipped. If it is set, and `confinement_exempt` is false, the kernel runs the DACL walk a third time, against the confinement identity:

- The user SID is replaced with `confinement_sid`.
- The groups list is replaced with the `confinement_capabilities` list.
- Group attributes are ignored — presence-based matching only.
- Owner implicit rights are **not** applied (the confined application is not the owner).

The result is `granted_confinement` — what the DACL would grant to a "fresh" caller whose entire identity is the confinement SID plus the declared capabilities. This is intersected with whatever the running grant is at this point.

**Privileges do not bypass this pass.** Bits granted by privileges in steps 4 or 9 are subject to the confinement intersection like any other bit. A confined token with `SeBackup` enabled and `BACKUP_INTENT` set can have its privilege grant stripped by confinement.

This is the major difference from the restricted-token pass. Restricted tokens trust their own author to use privileges responsibly; confinement does not trust the confined code at all. Confinement is policy applied *to* the application, not by it, so the application cannot escape via privilege exercise. See [Confinement](/peios/security-fundamentals/confinement/overview.md).

The `confinement_exempt` flag opts a specific token out of the confinement check. It is set very rarely — only by tools that legitimately need to step outside their own confinement.

## CAAP evaluation

Central access policies are the third narrowing layer. The mechanism: each `SYSTEM_SCOPED_POLICY_ID` ACE in the object's SACL references a policy by SID. The kernel looks up each policy in its policy cache (populated by authd) and evaluates the policy's rules.

A policy contains zero or more rules, each with:

- An optional **applies-to** expression — a conditional expression evaluated against the object's resource attributes. If the expression evaluates UNKNOWN or FALSE, the rule is skipped for this object. If it evaluates TRUE (or is absent), the rule applies.
- An **effective DACL** — a DACL that gets evaluated through a recursive sub-AccessCheck against the calling token. The result is intersected with the running grant.
- An optional **effective SACL** — audit ACEs that are collected for the eventual audit walk.
- Optional **staged DACL / staged SACL** — same content, but evaluated in parallel for testing purposes without affecting the actual grant.

For each applicable rule, AccessCheck recursively evaluates the effective DACL using a synthetic SD (where the DACL is the policy's DACL, the SACL has `SYSTEM_SCOPED_POLICY_ID` ACEs stripped to prevent recursion, and the object's owner/group are preserved). The result is intersected with the running grant.

**Privileges do not bypass CAAP** — bits granted by privileges are subject to CAAP narrowing like every other bit. The reason: CAAP is administrator-imposed policy, layered on top of object-level DACLs, and a privilege that bypassed it would defeat the model.

**Staging mismatches are surfaced separately.** If the staged DACL would have produced a different result, the access check sets the `staging_mismatch` flag in its output. The actual granted mask uses the effective DACL only — the staged DACL never affects what the caller gets. The flag is informational, for policy testing and rollout.

**The recovery policy** is the kernel's fallback when a referenced policy SID is not in the cache. Rather than fail-open (grant everything as if the policy were absent) or fail-closed (deny everything), the kernel applies a hardcoded recovery policy that grants `GENERIC_ALL` to `BUILTIN\Administrators`, `SYSTEM`, and `OWNER_RIGHTS`. This way, administrative access survives a missing-policy condition; ordinary access does not.

The full mechanism — applies-to expressions, staged vs effective semantics, distribution from authd — lives in [Central access policies](/peios/security-fundamentals/central-access-policies/overview.md). For this page, what matters is that CAAP is the last narrowing layer, that it is privilege-blind, and that it produces the staging-mismatch signal.

## Composition

When more than one narrowing layer is active, they compose in pipeline order:

```mermaid
flowchart LR
    A["Step 8 grant (DACL + step-4 privileges)"] -->|step 9| B["+ SeTakeOwnership if needed"]
    B -->|step 10| C["intersect with restricted pass (if restricted)"]
    C -->|step 4/9 privileges restored| D["intermediate grant"]
    D -->|step 11| E["intersect with confinement pass (if confined)"]
    E -->|step 12| F["intersect with each CAAP rule"]
    F --> G["final granted mask"]
```

A token can be all three at once: restricted, confined, and accessing an object with CAAP references. Each layer fires; the running grant is intersected three times; the final grant is what survives all of them. Each is conservative — they can only remove. A right that survives is one that all three layers permitted, on top of whatever the DACL and privileges produced.

A token can be one and not the others. A non-restricted, non-confined token accessing a CAAP-bound object faces only step 12. A restricted but not confined token accessing a non-CAAP object faces only step 10. The skipped steps are no-ops.

## Practical examples

**Restricted token, no confinement, no CAAP.** A sandbox launcher created a restricted version of its own token and ran the workload as that. The workload's accesses go through the DACL walk normally, then through the restricted pass (intersection against `restricted_sids`), then through confinement (no-op, token is not confined), then CAAP (no-op, object has no policy). Privilege-granted bits from steps 4/9 are restored after the restricted pass — the workload can still exercise its privileges.

**Confined application, no restricted SIDs, no CAAP.** A service was launched with confinement policy attached by the service manager. The accesses go through the DACL walk normally, then skip the restricted pass (no `restricted_sids`), then through confinement (intersection against the confinement identity). Privilege-granted bits are subject to the intersection — if the confinement DACL would not have granted them, they are lost.

**Plain DACL'd object with CAAP, ordinary token.** A file with a `SYSTEM_SCOPED_POLICY_ID` ACE in its SACL. The DACL walk produces an initial grant. Restricted and confinement passes are no-ops. CAAP looks up the referenced policy, evaluates each applicable rule, intersects each rule's effective DACL with the running grant. The final grant is the DACL-walk result narrowed by every applicable CAAP rule.

**All three.** A restricted, confined token accessing a CAAP-bound object. Every narrowing layer is active. The final grant has to survive all of them. This is rare in practice — restricted and confined are both belts-and-braces approaches — but it is what the model permits.

## What the narrowing layers do not do

A few clarifications that come up:

- **They do not run on objects that have no SACL.** Restricted tokens and confinement narrowing depend on the **token**, not on the SACL. The DACL walk happens; the restricted/confinement pass runs based on token state. Only CAAP requires SACL ACEs to fire.
- **They do not change the DACL itself.** The narrowing layers compute a *result*, not a rewritten DACL. The same DACL on the same object produces different results for a normal token vs a restricted one vs a confined one — and the DACL itself is unchanged.
- **They do not affect audit.** Audit ACEs in the SACL fire based on the *final* granted mask, not on what was granted by the DACL walk alone. An audit ACE that fires on success will fire only if the right survives all narrowing layers and ends up granted. A failure ACE fires only if the right would have been granted by some earlier layer but was stripped by a later one.
- **They do not affect outputs other than the granted mask.** Restricted-token and confinement passes do not produce their own audit events. CAAP produces the staging-mismatch flag and contributes effective SACL ACEs to the audit walk; it does not produce a separate "CAAP narrowed this" event.

## Why three different layers?

A reasonable question: if all three are intersections, why not just one? The reason is that they are intersections against different things, set by different audiences, with different bypass rules.

| Layer | Set by | Intersects against | Privileges bypass? |
|---|---|---|---|
| Restricted token | The application (in code) | The application's own restricted SID list | **Yes** |
| Confinement | The system administrator (policy) | The confinement SID + declared capabilities | No |
| CAAP | The directory administrator (policy) | Centrally-defined access rules | No |

The three sources of narrowing — the application, the local administrator, the directory — produce different layers because they answer different questions. "Does this code restrict its own authority?" is a programming concern; "is this service running in a sandbox?" is a deployment concern; "is this resource under enterprise data-protection policy?" is an organisational concern. Each layer handles its question.

The privilege-bypass split is the most consequential difference. Restricted tokens are application-internal and the application is trusted to use privileges responsibly. Confinement and CAAP are external policy and the bypassed code is not trusted; privileges must not be an escape route.

## Where to go next

For the systematic walk when an access comes back denied and you need to know which layer did it, read [Debugging a denial](/peios/security-fundamentals/access-decisions/debugging-a-denial.md).

For the token-side mechanics of the restricted pass, read [Restricted and write-restricted tokens](/peios/security-fundamentals/tokens/restricted-tokens.md).

---

# Debugging a denial

_Peios / Peios Security Fundamentals / Access Decisions_

> An access was denied and you need to know why — the systematic walk through the pipeline, what to inspect, in what order, and what each finding means.

A call returned `ACCESS_DENIED`, or a file open failed with `EACCES`, or a registry read came back empty. The access check decided "no", and you need to know which layer of the pipeline produced the denial. What follows is the systematic walk.

The pipeline has many steps, but the practical answer almost always falls in one of six categories. This page covers them in the order to check, with the inspection mechanics for each.

## The six places a denial can come from

Before stepping through, the list of candidates:

1. **The token cannot be used.** The thread is impersonating at the Identification level (step 0).
2. **MIC denied it.** The caller's integrity level is below the object's mandatory label, and the policy bits cover the requested category (step 5).
3. **PIP denied it.** The caller's process trust label does not dominate the object's, and the requested right is outside the explicitly-allowed mask (step 5).
4. **The DACL did not grant it.** The walk produced an empty result for the requested bits (step 8).
5. **A narrowing layer stripped it.** Restricted-token pass, confinement, or CAAP removed bits the DACL had granted (steps 10–12).
6. **A privilege was needed and not present, or present and not enabled, or present and missing its intent flag.** Steps 4 and 9 did not pre-decide the bits as granted.

For each of these, this page covers what to look at and what the finding means.

## Find the thread and its tokens

Almost every investigation starts with finding the thread that made the call and inspecting its tokens. The mechanics:

- The thread's primary token: `cat /proc/<pid>/task/<tid>/token` produces a query-only handle (or use `kacs_open_thread_token` programmatically). The handle can be queried with `KACS_IOC_QUERY` for fields including `user_sid`, `groups`, `integrity_level`, `privileges`, `restricted_sids`, `confinement_sid`, `auth_id`, and `impersonation_level`.
- The thread's effective token (impersonation if installed, primary otherwise): `cat /sys/kernel/security/kacs/self` when run from the thread itself; for another thread, the same `/proc/<pid>/task/<tid>/token` path returns the effective token.

For a non-self thread, you need `PROCESS_QUERY_INFORMATION` on the process and PIP dominance. For `/proc/self/token` and `/sys/kernel/security/kacs/self`, the kernel always permits inspection.

With the effective token in hand, several denials are immediately diagnosable:

- **`impersonation_level == Identification`** — the thread is at the wrong impersonation level. AccessCheck on this token denies everything at step 0. Either the client did not request a higher level on the socket, or the two-gate model silently downgraded the impersonation. See [The two-gate model](/peios/security-fundamentals/impersonation/the-two-gates.md).
- **`integrity_level` is low and the object is high** — possible MIC denial; check the object's label next.
- **`restricted_sids` is non-empty** — the token is restricted. The denial may be from the restricted-token pass; check what SIDs are in the restricted list.
- **`confinement_sid` is set and `confinement_exempt` is false** — the token is confined. The denial may be from the confinement pass.
- **A privilege you expected to be enabled is in the `present` set but not the `enabled` set** — the privilege is not active; AccessCheck cannot use it.
- **A privilege you expected to be on the token is absent entirely** — authd did not include it; this is a privilege-policy question, not an access-check question.

## Find the object and its SD

Read the security descriptor of the object:

- For files: `kacs_get_sd` on the path. Returns the SD as a self-relative binary blob.
- For registry keys: the equivalent registry API on the key.
- For tokens: query the token handle with the appropriate KACS_IOC_QUERY class.
- For processes: query the PSB.

Parse the SD into its four components (owner, group, DACL, SACL).

The most useful things to read:

- **The owner SID.** Does the caller match? If yes, owner implicit rights apply (unless suppressed — see next).
- **OWNER RIGHTS suppression.** Is there an ACE on `S-1-3-4` (OWNER RIGHTS) in the DACL? If so, the owner's implicit `READ_CONTROL | WRITE_DAC` is suppressed; the owner gets only what that ACE grants.
- **The DACL.** What ACEs are present, in what order? Are there any `INHERIT_ONLY` ACEs that look like they should grant access but are actually skipped during the walk?
- **The mandatory integrity label.** Look for a `SYSTEM_MANDATORY_LABEL_ACE` in the SACL. The SID indicates the object's integrity level; the mask indicates the policy bits.
- **The PIP trust label.** A `SYSTEM_PROCESS_TRUST_LABEL_ACE` in the SACL marks the object as PIP-protected.
- **CAAP references.** `SYSTEM_SCOPED_POLICY_ID_ACE` entries point to central access policies that contribute to the decision.
- **Resource attributes.** `SYSTEM_RESOURCE_ATTRIBUTE_ACE` entries. Relevant if any of the DACL's conditional ACEs references `@Resource.*`.

## Walk the pipeline

With token and SD in hand, walk the pipeline. The questions, in order:

### Impersonation level (pipeline step 0)

Is the effective token an impersonation token at the Identification level? If yes, that is the denial. AccessCheck stops at step 0; no further evaluation occurs. The fix is at the client end: either the client requested Identification (and should have requested Impersonation), or the two-gate model downgraded a higher request to Identification (consult [The two-gate model](/peios/security-fundamentals/impersonation/the-two-gates.md)).

### MIC (pipeline step 5)

Is the object's effective mandatory label at a higher level than the token's `integrity_level`?

If yes, look at the label's policy bits. For the bits set, the corresponding rights are pre-decided as denied:

- `NO_READ_UP` → read-category bits are denied.
- `NO_WRITE_UP` → write-category bits are denied (the default for unlabelled objects).
- `NO_EXECUTE_UP` → execute-category bits are denied.

If the requested right falls in a denied category, MIC is the denial. The fix is to either raise the token's integrity (typically by re-authenticating with elevated rights via UAC-style flow) or lower the object's label (requires `SeRelabelPrivilege` for raising, normal SACL access for lowering). Note that a privilege grant from step 4 (SeBackup, SeRestore) **survives** MIC — if MIC blocked write but the caller had `SeRestorePrivilege` with `RESTORE_INTENT`, the write may still succeed.

### PIP (pipeline step 5)

Does the object have a `SYSTEM_PROCESS_TRUST_LABEL_ACE`? If so, look at its SID's type and trust levels and compare to the calling process's PSB.

For dominance: both `pip_type` and `pip_trust` on the caller must be at least the ACE's values. If not, only the rights in the ACE's mask are permitted, and **privilege-granted bits are revoked** (this is the difference between PIP and MIC).

If PIP denied a right, the fix is to run the caller as a more-trusted binary (PIP is about the binary's signature, not about the caller's identity). See [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md).

### Privileges (pipeline steps 4 and 9)

Did the caller need a privilege to get the right? For example, the right is `ACCESS_SYSTEM_SECURITY` (SACL access), or the DACL granted nothing and the caller would need `SeBackup` to read.

Check:

- Is the privilege present on the token?
- Is it enabled?
- Did the caller pass the appropriate intent flag (`BACKUP_INTENT`, `RESTORE_INTENT`)?

If any of those is no, the privilege did not fire. The fix depends on which:

- Privilege absent → authd's privilege policy does not grant this privilege to this principal. A policy-level change, not an access-control change.
- Privilege present but disabled → the calling code should enable it via AdjustPrivileges before the operation.
- Privilege enabled but no intent flag → the calling code should pass the flag in `privilege_intent` to AccessCheck.

### The DACL walk (pipeline step 8)

Run the DACL walk manually, paying attention to:

- **Ordering.** Is the DACL in canonical order (explicit deny, explicit allow, inherited deny, inherited allow)? An out-of-order DACL produces surprising first-writer-wins results.
- **`INHERIT_ONLY` ACEs.** Are there ACEs that look relevant but have the `IO` flag set? They are skipped during the walk.
- **The matching identity.** Does the caller match the SIDs in the ACEs? Remember:
  - The token's `user_sid` matches.
  - Each group in `groups` with `SE_GROUP_ENABLED` set matches for allow ACEs.
  - Groups with `SE_GROUP_USE_FOR_DENY_ONLY` set match for deny ACEs only.
  - The well-known SIDs Everyone, Authenticated Users, etc. match according to their semantics.
  - `OWNER RIGHTS` and `PRINCIPAL_SELF` match per the [virtual group injection](/peios/security-fundamentals/security-descriptors/ownership.md) rules.
- **Conditional ACEs.** If any ACE has a conditional expression, evaluate it against the token's claims, the object's resource attributes, and the local claims (if any). A conditional that should evaluate TRUE but evaluates UNKNOWN (because a referenced attribute is missing) does not grant access for an allow ACE.

After the walk, the result is the bits the DACL would grant. If the requested right is missing from this result, the DACL is the denial.

### Narrowing layers (pipeline steps 10–12)

If the DACL granted the right but the final result does not have it, a narrowing layer stripped it. Check each:

- **Restricted-token pass.** If `restricted_sids` is non-empty, walk the DACL using only those SIDs. If the restricted-only result lacks the right, the restricted pass stripped it. (Privileges restored after this pass — privilege-granted bits are immune.)
- **Confinement.** If the token is confined, walk the DACL using `confinement_sid` and `confinement_capabilities`. If that result lacks the right, confinement stripped it. (Privileges are not restored — they can be lost here.)
- **CAAP.** If the SACL has `SYSTEM_SCOPED_POLICY_ID_ACE` entries, look up each referenced policy and evaluate its rules. If any rule's effective DACL would not have granted the right, CAAP stripped it. The recovery policy applies if a referenced policy is missing from the kernel cache.

### Audit (pipeline steps 13–14)

If the steps above did not produce a clear answer, check the audit log. The access check emits audit events that record:

- The requested mask, the granted mask.
- Which privileges contributed (and whether they survived).
- The matched ACE (for SACL-driven audits).
- The subject and process.

A `privilege-use` event with `success=false` tells you a privilege fired and was stripped — that points at confinement/CAAP/PIP. An `access-audit` event with `success=false` and a DACL-walk trigger tells you the DACL did not grant. The audit trail is often the fastest way to localise a denial.

## A compact checklist

When a denial happens, in order:

1. Find the thread (`tid`, `pid`).
2. Read the effective token. Note `user_sid`, `integrity_level`, `restricted_sids`, `confinement_sid`, `impersonation_level`, and which privileges are present-and-enabled.
3. Read the object's SD. Note the owner, the DACL contents (with attention to order and `INHERIT_ONLY`), the mandatory label, the PIP label, and any CAAP references or conditional ACEs.
4. Walk the pipeline mentally against these inputs.
5. Check the audit log if the manual walk does not point at a single layer.

Most denials are diagnosed at step 4 with one of the six categories listed at the top. The remainder are either edge cases (a conditional ACE evaluating UNKNOWN because of a missing claim, a CAAP policy with a misconfigured applies-to expression) or programming bugs (the wrong token installed, an incorrect intent flag, a hand-built SD with bad ordering).

## When to use the access-check syscall directly

For complex investigations, calling `kacs_access_check` directly is the precise tool. The syscall takes the token, the SD, the desired mask, and all the optional parameters (privilege_intent, self_sid, local_claims, object_audit_context, pip_type/pip_trust). It returns the granted mask, the continuous-audit mask, and the staging-mismatch flag.

You can call it from a debugging tool with the exact inputs the failing code used and see what comes back. The granted mask tells you which bits ended up granted; the audit emissions tell you which layers fired. A denial that is inscrutable from logs becomes visible from a manual access-check invocation with a known set of inputs.

This is how authoritative diagnoses work for the hard cases: reproduce the access check inputs, call the syscall manually, examine the output.

## Where to go next

For the inspection surfaces this walk relies on — reading tokens, sessions, and process state on a live system — read [Inspecting tokens, sessions, and processes](/peios/security-fundamentals/inspecting/overview.md).

To rehearse an access check from a shell against a real file and token, read [The sd command](/peios/security-fundamentals/security-descriptors/sd-command.md).
