On this page
Security descriptors
A security descriptor in wire form uses the self-relative layout — a single contiguous byte buffer with the components (owner, group, DACL, SACL) stored at offsets specified in the header. The format is self-contained: no external pointers, no fixup needed on transport.
This page covers the byte-level layout. The conceptual model of SDs is in Security descriptors; this page is the parser/encoder reference.
All multi-byte integers are little-endian on x86_64.
SD header
Every SD starts with a 20-byte header:
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 1 | Revision |
0x01 for v0.20. |
| 1 | 1 | Sbz1 |
Reserved; must be 0 unless SE_RM_CONTROL_VALID is set (in which case this is the resource-manager control byte, opaque to KACS). |
| 2 | 2 | Control |
Flags; see below. |
| 4 | 4 | OffsetOwner |
Offset (from SD start) to the owner SID. 0 if absent. |
| 8 | 4 | OffsetGroup |
Offset to the primary group SID. 0 if absent. |
| 12 | 4 | OffsetSacl |
Offset to the SACL. 0 if absent. |
| 16 | 4 | OffsetDacl |
Offset to the DACL. 0 if absent. |
Total: 20 bytes. Variable-length component data follows.
Control flags (16 bits)
The Control field is a bitmask:
| Bit | Name | Meaning |
|---|---|---|
| 0x0001 | SE_OWNER_DEFAULTED |
The owner SID was defaulted (not explicitly set by user). |
| 0x0002 | SE_GROUP_DEFAULTED |
Same for primary group. |
| 0x0004 | SE_DACL_PRESENT |
The DACL is present. If clear, the SD has a NULL DACL (grants all). |
| 0x0008 | SE_DACL_DEFAULTED |
The DACL was defaulted. |
| 0x0010 | SE_SACL_PRESENT |
The SACL is present. |
| 0x0020 | SE_SACL_DEFAULTED |
The SACL was defaulted. |
| 0x0040 | SE_DACL_TRUSTED |
(Informational; KACS doesn't act on this.) |
| 0x0080 | SE_SERVER_SECURITY |
Server-security mode. Fail-closed in v0.20 — SDs with this set are rejected. |
| 0x0100 | SE_DACL_AUTO_INHERIT_REQ |
Auto-inheritance was requested for the DACL. |
| 0x0200 | SE_SACL_AUTO_INHERIT_REQ |
Same for SACL. |
| 0x0400 | SE_DACL_AUTO_INHERITED |
The DACL was auto-inherited. |
| 0x0800 | SE_SACL_AUTO_INHERITED |
Same. |
| 0x1000 | SE_DACL_PROTECTED |
The DACL is protected — does not accept inherited ACEs from parent. |
| 0x2000 | SE_SACL_PROTECTED |
Same for SACL. |
| 0x4000 | SE_RM_CONTROL_VALID |
The Sbz1 byte is a resource-manager control byte. |
| 0x8000 | SE_SELF_RELATIVE |
The SD is in self-relative format. Always set on the wire. |
Bit interactions worth noting:
SE_DACL_PRESENT = 0means NULL DACL (grant all).SE_DACL_PRESENT = 1with zero ACEs in the DACL means empty DACL (grant none).SE_SELF_RELATIVEmust be set on any SD passed across the kernel boundary. An SD without it would be in absolute (pointer-based) format, which is not valid on the wire.
Offsets
The four offsets (OffsetOwner, OffsetGroup, OffsetSacl, OffsetDacl) point into the buffer from the SD's start. An offset of 0 means the component is absent.
The kernel validates:
- All non-zero offsets must point within the SD buffer.
- Each component must fit within the SD buffer.
- Components must not overlap each other or the header.
- Absent components must have offset 0 and the corresponding PRESENT control flag clear (for DACL/SACL).
Components are typically laid out in a stable order (header, owner SID, group SID, SACL, DACL) but the format allows any non-overlapping arrangement.
SID encoding
A SID in the SD (and elsewhere):
| Bytes | Field | Encoding |
|---|---|---|
| 0 | Revision |
0x01 |
| 1 | SubAuthorityCount |
0–15 |
| 2–7 | IdentifierAuthority |
6 bytes, big-endian |
| 8 onward | SubAuthorities |
4 bytes each, little-endian |
Total size: 8 + 4 × SubAuthorityCount bytes (8 minimum, 68 maximum).
The big-endian authority is the SID format's one exception to the little-endian rule. Sub-authorities revert to little-endian.
ACL encoding
A DACL or SACL has an 8-byte header followed by ACEs:
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 1 | AclRevision |
0x02 (basic) or 0x04 (DS — supports object/callback ACEs). |
| 1 | 1 | Sbz1 |
Reserved; 0. |
| 2 | 2 | AclSize |
Total ACL size in bytes (header + ACEs). Max 64 KB. |
| 4 | 2 | AceCount |
Number of ACEs. |
| 6 | 2 | Sbz2 |
Reserved; 0. |
After the header, AceCount ACEs are packed back-to-back without padding (each ACE's AceSize is a multiple of 4 bytes).
ACE encoding
Every ACE starts with a 4-byte header:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | AceType |
| 1 | 1 | AceFlags |
| 2 | 2 | AceSize (multiple of 4) |
The body's shape depends on AceType.
AceType values
| Value | Name | Body shape |
|---|---|---|
| 0x00 | ACCESS_ALLOWED |
Single-SID |
| 0x01 | ACCESS_DENIED |
Single-SID |
| 0x02 | SYSTEM_AUDIT |
Single-SID |
| 0x03 | SYSTEM_ALARM |
Single-SID |
| 0x04 | (reserved) | — |
| 0x05 | ACCESS_ALLOWED_OBJECT |
Object |
| 0x06 | ACCESS_DENIED_OBJECT |
Object |
| 0x07 | SYSTEM_AUDIT_OBJECT |
Object |
| 0x08 | SYSTEM_ALARM_OBJECT |
Object |
| 0x09 | ACCESS_ALLOWED_CALLBACK |
Callback (single-SID + expression) |
| 0x0A | ACCESS_DENIED_CALLBACK |
Callback |
| 0x0B | ACCESS_ALLOWED_CALLBACK_OBJECT |
Callback object |
| 0x0C | ACCESS_DENIED_CALLBACK_OBJECT |
Callback object |
| 0x0D | SYSTEM_AUDIT_CALLBACK |
Callback |
| 0x0E | SYSTEM_ALARM_CALLBACK |
Callback |
| 0x0F | SYSTEM_AUDIT_CALLBACK_OBJECT |
Callback object |
| 0x10 | SYSTEM_ALARM_CALLBACK_OBJECT |
Callback object |
| 0x11 | SYSTEM_MANDATORY_LABEL |
Single-SID |
| 0x12 | SYSTEM_RESOURCE_ATTRIBUTE |
Single-SID + claim entry |
| 0x13 | SYSTEM_SCOPED_POLICY_ID |
Single-SID |
| 0x14 | SYSTEM_PROCESS_TRUST_LABEL |
Single-SID |
AceFlags
The AceFlags byte:
| Bit | Name | Meaning |
|---|---|---|
| 0x01 | OBJECT_INHERIT_ACE |
Inherits to non-container children. |
| 0x02 | CONTAINER_INHERIT_ACE |
Inherits to container children. |
| 0x04 | NO_PROPAGATE_INHERIT_ACE |
Inherits one level only. |
| 0x08 | INHERIT_ONLY_ACE |
Applies to children only, not this object. |
| 0x10 | INHERITED_ACE |
This ACE was created by inheritance. |
| 0x40 | SUCCESSFUL_ACCESS_ACE_FLAG |
(Audit/alarm) Fire on success. |
| 0x80 | FAILED_ACCESS_ACE_FLAG |
(Audit/alarm) Fire on failure. |
Single-SID ACE body
For ACE types 0x00–0x03, 0x11, 0x12, 0x13, 0x14:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Header (AceType, AceFlags, AceSize) |
| 4 | 4 | Mask |
| 8 | variable | SID (binary form, consumes remainder of ACE) |
For SYSTEM_RESOURCE_ATTRIBUTE_ACE (0x12), the SID is always S-1-1-0 (Everyone), and additional ApplicationData (one claim entry) follows. The claim entry format is in Token and session specs.
Object ACE body
For ACE types 0x05–0x08:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Header |
| 4 | 4 | Mask |
| 8 | 4 | Flags (bit 0x01 = ObjectType present; bit 0x02 = InheritedObjectType present) |
| 12 | 16 | ObjectType GUID (if Flags & 0x01) |
| 28 | 16 | InheritedObjectType GUID (if Flags & 0x02) |
| variable | SID |
GUID fields are present only when the corresponding flag bit is set. If both are absent, the SID starts at offset 12; if only ObjectType is present, SID starts at 28; etc.
Callback ACE body (non-object)
For ACE types 0x09, 0x0A, 0x0D, 0x0E:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Header |
| 4 | 4 | Mask |
| 8 | variable | SID |
| variable | variable | ApplicationData (conditional ACE bytecode) |
The ApplicationData consumes the rest of the ACE after the SID. The format starts with the four magic bytes 0x61 0x72 0x74 0x78 ("artx") and contains conditional ACE bytecode — see Conditional ACE bytecode.
Callback object ACE body
For ACE types 0x0B, 0x0C, 0x0F, 0x10: same as object ACE plus trailing ApplicationData.
Access mask layout
The 32-bit access mask used in every ACE:
| Bits | Region | Examples |
|---|---|---|
| 0–15 | Object-specific rights | FILE_READ_DATA, PROCESS_TERMINATE, TOKEN_QUERY, etc. (16 type-specific bits) |
| 16–20 | Standard rights | DELETE (0x10000), READ_CONTROL (0x20000), WRITE_DAC (0x40000), WRITE_OWNER (0x80000), SYNCHRONIZE (0x100000) |
| 21–23 | Reserved | Must be 0 |
| 24 | ACCESS_SYSTEM_SECURITY |
0x01000000 |
| 25 | MAXIMUM_ALLOWED |
0x02000000 |
| 26–27 | Reserved | Must be 0 |
| 28–31 | Generic rights | GENERIC_ALL (0x10000000), GENERIC_EXECUTE (0x20000000), GENERIC_WRITE (0x40000000), GENERIC_READ (0x80000000) |
The object-specific bits (0–15) have meaning that depends on the type of object the SD is on. The full per-object-type bit catalogs are in Constants and catalogs.
The generic bits are expanded at evaluation time via the object type's GenericMapping table. They never appear in stored SDs as final rights; the kernel maps them before walking.
Limits
| Limit | Value |
|---|---|
| Max SD size | 65,535 bytes |
| Max ACL size | 64 KB (16-bit AclSize) |
| Max ACEs per ACL | Bounded by AclSize and per-ACE size |
| Min SID size | 8 bytes (revision + count + authority, no sub-authorities) |
| Max SID size | 68 bytes (15 sub-authorities) |
ACE AceSize granularity |
Multiple of 4 bytes |
The kernel rejects SDs that exceed these limits or have malformed internal sizes with -EINVAL.
Self-relative round trips
Because the format is self-contained and has no embedded pointers, an SD blob can be:
- Stored verbatim in an xattr.
- Sent over the wire (a domain controller replicating an SD to a member system).
- Backed up byte-for-byte and restored.
The validation rules are also self-contained — the kernel can validate an SD without external state. The same blob is valid on every Peios system, regardless of the deployment.
This is the property that makes SDs portable. They are not "for this machine"; they are universally interpretable.