# ACLs, ACEs, and access masks

_Peios / Peios Security Fundamentals / Security Descriptors_

> The shared structure of DACLs and SACLs — ACL and ACE layout, the catalog of ACE types, the inheritance and audit flags, and the 32-bit access mask.

A DACL and a SACL are both **Access Control Lists** — sequences of **Access Control Entries** in a defined order. The two lists have different jobs (the DACL decides access; the SACL records system-level policy and audit), but they share a structure. This page covers that structure, plus the catalog of ACE types and the layout of the 32-bit access mask that almost every ACE carries.

## ACL structure

An ACL is, conceptually, an ordered array of ACEs. The binary form is a short header — a revision byte, the total `AclSize`, and the `AceCount` — followed by the ACEs back-to-back; the byte-level layout is owned by the [Security descriptors wire format](/peios/using-peios/wire-formats-reference/security-descriptors.md).

The maximum ACL size is 64 KB — the AclSize field is a 16-bit value. An ACL is self-delimiting: a parser walks exactly `AceCount` ACEs starting after the header and stops when it has consumed `AclSize` bytes.

The two ACL revisions differ only in what ACE types they are allowed to contain. `0x02` is the basic revision; `0x04` additionally allows object ACEs (the type with property GUIDs) and callback ACEs (the type with conditional expressions). Parsers should accept whichever revision they see; writers should emit the minimum revision necessary for the ACEs they are including.

## ACE structure

Every ACE has a 4-byte header — a one-byte `AceType`, a one-byte `AceFlags`, and a two-byte `AceSize` (always a multiple of 4) — followed by a type-specific body. The body's shape depends on the type. The most common shapes:

- **Single-SID body** — `Mask (4 bytes) | SID (variable)`. Used by basic allow/deny/audit/alarm ACEs and by the integrity/scoped-policy/trust-label SACL types.
- **Object ACE body** — `Mask (4) | Flags (4) | optional ObjectType GUID (16) | optional InheritedObjectType GUID (16) | SID (variable)`. The flags field controls whether each GUID is present.
- **Callback (conditional) body** — same as the corresponding non-callback type, plus a trailing ApplicationData block holding the conditional expression bytecode. See [Conditional ACEs](/peios/security-fundamentals/security-descriptors/conditional-aces.md).

The detailed byte-level layouts live in the [Wire formats reference](/peios/using-peios/wire-formats-reference/overview.md). For this page, what matters is that every ACE is self-describing — the parser reads the header, dispatches by type, and consumes exactly `AceSize` bytes.

## The ACE type catalog

There are twenty-one ACE types, split into three families: access-control, audit/alarm, and system-policy. Most SDs you will look at use only a small subset of these — typically `ACCESS_ALLOWED`, `ACCESS_DENIED`, `SYSTEM_AUDIT`, and `SYSTEM_MANDATORY_LABEL`. The numeric type values are catalogued in [ACE types and flags](/peios/using-peios/constants-and-catalogs/ace-types-and-flags.md); the tables here describe what each type is for.

### Access-control ACE family

These ACEs participate in the DACL walk.

| Type | Body | Use |
|---|---|---|
| `ACCESS_ALLOWED` | Single SID | Grant the listed rights to the named principal. |
| `ACCESS_DENIED` | Single SID | Deny the listed rights to the named principal. |
| `ACCESS_ALLOWED_COMPOUND` | Reserved (not used in v0.20). | — |
| `ACCESS_ALLOWED_OBJECT` | Object ACE | Grant rights, scoped by property GUID or inherited only by certain child object types. |
| `ACCESS_DENIED_OBJECT` | Object ACE | Deny rights, same scoping. |
| `ACCESS_ALLOWED_CALLBACK` | Single SID + expression | Grant rights only if the conditional expression evaluates TRUE. |
| `ACCESS_DENIED_CALLBACK` | Single SID + expression | Deny rights when the expression evaluates TRUE or UNKNOWN. |
| `ACCESS_ALLOWED_CALLBACK_OBJECT` | Object ACE + expression | Grant rights, conditionally, scoped by GUID. |
| `ACCESS_DENIED_CALLBACK_OBJECT` | Object ACE + expression | Deny rights, conditionally, scoped by GUID. |

The most common ACEs you will write are `ACCESS_ALLOWED` and `ACCESS_DENIED` — plain "this principal gets these rights" or "this principal does not get these rights". The object variants matter when you are protecting directory-style objects with per-property permissions. The callback variants are how conditional expressions show up in ACEs.

### Audit and alarm ACE family

These ACEs sit in the SACL and decide when access produces an audit event.

| Type | Body | Use |
|---|---|---|
| `SYSTEM_AUDIT` | Single SID | Emit an audit event when the named principal attempts the listed rights. Flags decide whether success, failure, or both. |
| `SYSTEM_AUDIT_OBJECT` | Object ACE | Same, scoped by GUID. |
| `SYSTEM_AUDIT_CALLBACK` | Single SID + expression | Audit conditionally. UNKNOWN result emits the event (fail-open for auditing). |
| `SYSTEM_AUDIT_CALLBACK_OBJECT` | Object ACE + expression | Conditional, scoped by GUID. |
| `SYSTEM_ALARM` | Single SID | Configure per-operation continuous audit on the open handle. Each matching operation produces an event. |
| `SYSTEM_ALARM_OBJECT` | Object ACE | Same, scoped by GUID. |
| `SYSTEM_ALARM_CALLBACK` | Single SID + expression | Conditional continuous audit. |
| `SYSTEM_ALARM_CALLBACK_OBJECT` | Object ACE + expression | Conditional, scoped by GUID. |

The distinction between AUDIT and ALARM is important: AUDIT fires at the moment of handle creation (one event per access attempt), ALARM configures a per-operation mask on the open handle that fires on every subsequent operation. See [The SACL](/peios/security-fundamentals/security-descriptors/the-sacl.md) and [Auditing](/peios/security-fundamentals/auditing/overview.md) for the full story.

### System-policy ACE family

These ACEs sit in the SACL and carry policy other than audit.

| Type | Body | Use |
|---|---|---|
| `SYSTEM_MANDATORY_LABEL` | Integrity SID + mask | The object's mandatory integrity label and the policy bits that govern who can write/read/execute up. |
| `SYSTEM_RESOURCE_ATTRIBUTE` | Everyone SID + claim entry | A name-value attribute on the object, referenceable as `@Resource.<name>` in conditional expressions. |
| `SYSTEM_SCOPED_POLICY_ID` | Policy SID | A reference to a central access policy that should be evaluated alongside the object's own DACL. |
| `SYSTEM_PROCESS_TRUST_LABEL` | PIP SID + mask | The object's PIP trust label and the explicit allowed mask for non-dominant callers. |

These four are how the SACL extends the access decision beyond what the DACL alone can express. Each has its own evaluation rules (covered in the relevant topics — MIC under [Access decisions](/peios/security-fundamentals/access-decisions/overview.md), resource attributes under [Resource attributes](/peios/security-fundamentals/security-descriptors/resource-attributes.md), CAAP under [Central access policies](/peios/security-fundamentals/central-access-policies/overview.md), PIP under [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md)).

## ACE flags

The `AceFlags` field is a bitmask. Most flags control inheritance — whether and how an ACE on a parent object propagates to children. Two flags control audit firing on SYSTEM_AUDIT and SYSTEM_ALARM ACEs. (Bit values: [ACE types and flags](/peios/using-peios/constants-and-catalogs/ace-types-and-flags.md).)

| Flag | Effect |
|---|---|
| `OBJECT_INHERIT_ACE` | The ACE inherits to non-container child objects (files, not directories). |
| `CONTAINER_INHERIT_ACE` | The ACE inherits to container child objects (directories). |
| `NO_PROPAGATE_INHERIT_ACE` | The ACE is inherited by direct children but its `OBJECT_INHERIT_ACE` and `CONTAINER_INHERIT_ACE` flags are cleared in the inherited copy — it stops propagating after one level. |
| `INHERIT_ONLY_ACE` | The ACE is for inheritance only. The access check skips it; it exists only to be copied to children. |
| `INHERITED_ACE` | Set on ACEs that were created by inheritance, not by an explicit add. Tools use this flag to distinguish "this came from the parent" from "the user set this explicitly". |
| `SUCCESSFUL_ACCESS_ACE_FLAG` | (Audit/alarm only) Fire on successful access. |
| `FAILED_ACCESS_ACE_FLAG` | (Audit/alarm only) Fire on denied access. |

The inheritance flags compose in non-obvious ways. Combinations you will see often:

| Flags | Effect |
|---|---|
| `CONTAINER_INHERIT_ACE \| OBJECT_INHERIT_ACE` (`CI \| OI`) | Inherit to all descendants (files and directories), recursively. |
| `CONTAINER_INHERIT_ACE` (`CI`) | Inherit to descendant containers only. |
| `OBJECT_INHERIT_ACE` (`OI`) | Inherit to descendant non-containers only. |
| `CI \| OI \| INHERIT_ONLY_ACE` | Apply to descendants but not to this object. |
| `CI \| OI \| NO_PROPAGATE_INHERIT_ACE` | Apply to immediate children only, not grandchildren. |

The full inheritance machinery is on [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md).

## The access mask

Every ACE that participates in the DACL walk, and every audit/alarm ACE, carries a 32-bit access mask. The mask uses four regions, partitioned by bit position.

| Region | Bits | Purpose |
|---|---|---|
| Object-specific rights | 0–15 | 16 bits whose meaning depends on the object type. For a file: `FILE_READ_DATA`, `FILE_WRITE_DATA`, etc. For a registry key: `KEY_QUERY_VALUE`, `KEY_SET_VALUE`, etc. For a process: `PROCESS_TERMINATE`, `PROCESS_VM_READ`, etc. |
| Standard rights | 16–20 | 5 bits shared across all object types: `DELETE`, `READ_CONTROL`, `WRITE_DAC`, `WRITE_OWNER`, `SYNCHRONIZE`. |
| Reserved | 21–23 | Must be zero. |
| Special rights | 24–25 | `ACCESS_SYSTEM_SECURITY` — gates SACL read/write. `MAXIMUM_ALLOWED` — a request flag, not a real right. |
| Reserved | 26–27 | Must be zero. |
| Generic rights | 28–31 | `GENERIC_ALL`, `GENERIC_EXECUTE`, `GENERIC_WRITE`, `GENERIC_READ`. Abstract — mapped to object-specific rights at evaluation time. |

The numeric values for every right live in [Access mask bits](/peios/using-peios/constants-and-catalogs/access-mask-bits.md); the mask's byte layout is in the [SD wire format](/peios/using-peios/wire-formats-reference/security-descriptors.md). A few things worth noting about the layout:

- **The same bit means different things on different object types.** Bit 0 is `FILE_READ_DATA` on a file but `KEY_QUERY_VALUE` on a registry key. The object-specific portion of the mask is reusable across types because the object manager knows what kind of object it is.
- **Standard rights are uniform.** `DELETE` always means delete the object; `READ_CONTROL` always means read the SD; `WRITE_DAC` always means modify the DACL; `WRITE_OWNER` always means change the owner. These bits work identically regardless of object type.
- **Generic rights are abstract.** `GENERIC_READ` does not name a specific right. It is a placeholder that gets mapped to a concrete combination of object-specific and standard rights at the moment of evaluation, using the object type's GenericMapping table.

### Generic mapping

When an access check sees a generic right in either an ACE mask or a requested mask, it consults the object type's GenericMapping table to substitute concrete rights. The mapping is per-type:

| Object type | GENERIC_READ maps to |
|---|---|
| File | `FILE_READ_DATA \| FILE_READ_ATTRIBUTES \| FILE_READ_EA \| READ_CONTROL \| SYNCHRONIZE` |
| Token | `TOKEN_QUERY \| READ_CONTROL` |
| Process | `PROCESS_QUERY_INFORMATION \| PROCESS_VM_READ \| READ_CONTROL` |

The full GenericMapping tables live in the [Constants and catalogs](/peios/using-peios/constants-and-catalogs/overview.md) reference. The key idea for now is: generic rights let an ACE author say "give the read permission for this thing" without knowing exactly which bits "read" means for this object type. The access check fills in the concrete bits at evaluation time.

This is also why an ACE that grants `GENERIC_READ` to a principal will produce different concrete grants depending on which object the ACE is on. The bit pattern in the ACE is the same; the meaning depends on where it lives.

### MAXIMUM_ALLOWED is a request, not a right

`MAXIMUM_ALLOWED` (bit 25) is special. It is not a right an ACE can grant — it is a marker the **caller** sets in a desired-access mask when asking "what is the maximum access I could be granted right now?". The access check sees the flag, evaluates the DACL fully, and returns every right the caller could be granted.

`MAXIMUM_ALLOWED` must not appear in an ACE. It is meaningless there.

## Sizes and limits

The size limits (maximum SD size, maximum ACL size, and the rest) are catalogued in [Other constants](/peios/using-peios/constants-and-catalogs/other-constants.md). A practical SD on a typical object is far below all of them — a few hundred bytes at most. The limits exist for the pathological cases.

## Where to go next

For how the ACEs in a DACL actually decide "allowed" or "denied" — first-writer-wins, ordering, the null/empty distinction — read [DACL evaluation](/peios/security-fundamentals/security-descriptors/dacl-evaluation.md).

For the full numeric catalog of ACE types, flags, and access-mask bits, see [ACE types and flags](/peios/using-peios/constants-and-catalogs/ace-types-and-flags.md).

To read and edit ACLs from a shell, read [The sd command](/peios/security-fundamentals/security-descriptors/sd-command.md).

Related content:

- [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md)
- [DACL evaluation](/peios/security-fundamentals/security-descriptors/dacl-evaluation.md)
- [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md)
- [The SACL](/peios/security-fundamentals/security-descriptors/the-sacl.md)
- [Conditional ACEs](/peios/security-fundamentals/security-descriptors/conditional-aces.md)
