Access Decisions
Single-page view · as markdown
Access decisions
Peios / Peios Security Fundamentals / Access Decisions
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 #
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. |
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. |
| Staging mismatch flag | Set when a staged CAAP policy would have produced a different result. See Central access policies. |
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. |
| 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. |
| 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 and Process integrity protection. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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, 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. 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.
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.
If you want the narrowing layers — restricted-token pass, confinement pass, CAAP — in detail, read Narrowing layers.
If you are debugging an unexpected denial right now, read Debugging a denial.
Mandatory integrity control
Peios / Peios Security Fundamentals / Access Decisions
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):
| 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-16SID with one sub-authority — normally one of the five above, occasionally a non-standard level such asmedium-plusorprotected). - 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):
| 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 #
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:
- Extract the object's integrity label (or apply the Medium-with-NO_WRITE_UP default if there is no label ACE).
- Compare the token's
integrity_levelagainst the object's label. - If the token's level is at least the object's level, MIC is satisfied. Nothing happens; the pipeline proceeds.
- 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
decidedset but notgranted.
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 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.
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 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 (
SeSecurityPrivilegeorSeRestorePrivilege). - 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'spip_typeandpip_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_ACEare not PIP-protected; objects without aSYSTEM_MANDATORY_LABEL_ACEget 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.
For the binary-trust axis MIC is so often compared with, read Process integrity protection.
Privileges in the pipeline
Peios / Peios Security Fundamentals / Access Decisions
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. 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). 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.
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_EAfor files; corresponding bits for other types). DELETE.WRITE_OWNERandWRITE_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_sdwithLABEL_SECURITY_INFORMATION(orSACL_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_OWNERwhen the caller is operating on an object with a higher integrity label — specifically, the privilege "punchesWRITE_OWNERthrough 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.
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 inaudit_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.
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.
For the privilege-use events themselves and where they end up, read Auditing.
Narrowing layers
Peios / Peios Security Fundamentals / Access Decisions
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 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_capabilitieslist. - 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.
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. 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:
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.
For the token-side mechanics of the restricted pass, read Restricted and write-restricted tokens.
Debugging a denial
Peios / Peios Security Fundamentals / Access Decisions
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:
- The token cannot be used. The thread is impersonating at the Identification level (step 0).
- 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).
- 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).
- The DACL did not grant it. The walk produced an empty result for the requested bits (step 8).
- A narrowing layer stripped it. Restricted-token pass, confinement, or CAAP removed bits the DACL had granted (steps 10–12).
- 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>/tokenproduces a query-only handle (or usekacs_open_thread_tokenprogrammatically). The handle can be queried withKACS_IOC_QUERYfor fields includinguser_sid,groups,integrity_level,privileges,restricted_sids,confinement_sid,auth_id, andimpersonation_level. - The thread's effective token (impersonation if installed, primary otherwise):
cat /sys/kernel/security/kacs/selfwhen run from the thread itself; for another thread, the same/proc/<pid>/task/<tid>/tokenpath 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.integrity_levelis low and the object is high — possible MIC denial; check the object's label next.restricted_sidsis non-empty — the token is restricted. The denial may be from the restricted-token pass; check what SIDs are in the restricted list.confinement_sidis set andconfinement_exemptis false — the token is confined. The denial may be from the confinement pass.- A privilege you expected to be enabled is in the
presentset but not theenabledset — 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_sdon 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 implicitREAD_CONTROL | WRITE_DACis suppressed; the owner gets only what that ACE grants. - The DACL. What ACEs are present, in what order? Are there any
INHERIT_ONLYACEs that look like they should grant access but are actually skipped during the walk? - The mandatory integrity label. Look for a
SYSTEM_MANDATORY_LABEL_ACEin 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_ACEin the SACL marks the object as PIP-protected. - CAAP references.
SYSTEM_SCOPED_POLICY_ID_ACEentries point to central access policies that contribute to the decision. - Resource attributes.
SYSTEM_RESOURCE_ATTRIBUTE_ACEentries. 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).
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.
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_intentto 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_ONLYACEs. Are there ACEs that look relevant but have theIOflag set? They are skipped during the walk.- The matching identity. Does the caller match the SIDs in the ACEs? Remember:
- The token's
user_sidmatches. - Each group in
groupswithSE_GROUP_ENABLEDset matches for allow ACEs. - Groups with
SE_GROUP_USE_FOR_DENY_ONLYset match for deny ACEs only. - The well-known SIDs Everyone, Authenticated Users, etc. match according to their semantics.
OWNER RIGHTSandPRINCIPAL_SELFmatch per the virtual group injection rules.
- The token's
- 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_sidsis 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_sidandconfinement_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_ACEentries, 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:
- Find the thread (
tid,pid). - Read the effective token. Note
user_sid,integrity_level,restricted_sids,confinement_sid,impersonation_level, and which privileges are present-and-enabled. - 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. - Walk the pipeline mentally against these inputs.
- 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.
To rehearse an access check from a shell against a real file and token, read The sd command.