3.8.2 The DACL Walk

The DACL is an ordered list of ACEs, walked from first to last, comparing each ACE's SID against the calling token's identity. The governing principle is first-writer-wins: once a bit has been resolved, granted or denied, no later ACE changes the outcome for that bit.

An allow ACE whose SID matches the token grants the rights it carries that have not yet been decided, leaving already-decided bits untouched. A deny ACE whose SID matches denies its not-yet-decided rights — marking them decided but not granted — and likewise leaves decided bits alone.

3.8.2.1 SID matching #

An ACE's SID matches the token when it equals the user SID or a group SID on the token, subject to attribute filtering.

For allow ACEs, only groups that are enabled and not deny-only match. For deny ACEs, both enabled groups and deny-only groups match: a deny-only group always participates in deny matching whatever its enabled state. A group with neither SE_GROUP_ENABLED nor SE_GROUP_USE_FOR_DENY_ONLY participates in no matching at all.

The user SID follows the same rule — when user_deny_only is set on the token, it matches deny ACEs and not allow ACEs.

3.8.2.2 Skipping and mapping #

ACEs carrying INHERIT_ONLY exist purely to propagate to child objects and are skipped by the walk.

At the top of the walk each ACE's access mask is mapped through MapGenericBits using the same GenericMapping applied to the caller's request. The mapping works on a local copy — the ACE itself is never mutated. Mapping ACE masks at evaluation time is a deliberate departure from MS-DTYP, and it is what makes GENERIC_ALL work in central access policy recovery ACEs (§3.8.8).

3.8.2.3 Absent and empty DACLs #

If the descriptor has no DACL — SE_DACL_PRESENT unset — every valid right not already decided by an earlier pipeline stage is granted. The valid rights are bounded by MapGenericBits(GENERIC_ALL, mapping) rather than by a raw 0xFFFFFFFF.

If the DACL is present but holds zero ACEs, the walk grants nothing. The only access an owner gets in that case comes from the implicit rights mechanism below.

3.8.2.4 Owner implicit rights #

By default the owner of an object receives READ_CONTROL and WRITE_DAC whatever the DACL says. These are granted before the walk begins, as the first action inside EvaluateDACL, and because first-writer-wins governs the walk, no deny ACE encountered later can override them.

EvaluateDACL takes a skip_owner_implicit parameter. The confinement pass sets it, because confinement is an absolute intersection with no owner bypass.

The grant is suppressed entirely if any non-inherit-only access-control ACE in the DACL targets the OWNER RIGHTS SID (S-1-3-4). This is a pre-scan performed at the start of EvaluateDACL, before the main loop, and it checks only for the SID's presence — it does not evaluate any conditional expression on the ACE.

During the walk proper, S-1-3-4 is treated as an ordinary SID matching the owner, at both allow and deny polarity. It obeys the same rules as any other SID: an allow ACE matches only through an enabled, non-deny-only group, and not through the user SID of a user_deny_only token; a deny ACE matches through a group that is enabled or deny-only, and through the user SID unconditionally.

Note that the implicit grant above is a separate rule and remains presence-based. It is bounded by the pre-scan rather than by polarity.

The implicit grant is also bounded twice over: by the object type's valid rights, and by what has already been decided. A pre-decision from MIC, PIP or a privilege therefore suppresses it — a non-dominant owner does not receive WRITE_DAC through this route.

3.8.2.5 MAXIMUM_ALLOWED #

When the caller includes MAXIMUM_ALLOWED (bit 25), AccessCheck runs the full pipeline and returns the complete set of rights that would be granted. The bit is stripped from the desired mask before evaluation begins.

Two things change. The walk runs to completion with no short-circuit, and the returned mask is whatever the pipeline accumulated rather than being filtered to the requested bits.

MAXIMUM_ALLOWED can be combined with specific rights: MAXIMUM_ALLOWED | READ_CONTROL asks both "can I read the descriptor?" as a success or failure and "what else could I get?" as a mask. A pure MAXIMUM_ALLOWED request carrying no specific bits always succeeds.

Otherwise — when the desired mask is fully decided — the walk may stop early.

First-writer-wins applies to MAXIMUM_ALLOWED requests exactly as it does to targeted ones. MS-DTYP treats the two differently; KACS does not, which is what stops "what can I do?" and "can I do this?" disagreeing on a DACL that is not in canonical order.

Edit this page