# Peios Common Data Structures

> The common binary data structures shared across Peios subsystems: the GUID, LUID, and SID identifier types, and the security descriptor family.

---

# 1.1 Scope

_Peios / Advanced Peios / PCDS / Introduction_

> What PCDS defines — GUID, LUID, SID and the Security Descriptor family — and what it deliberately leaves to the Peios Kernel TRM.

This document defines the common binary data structures shared across
Peios subsystems: three identifier types — the Globally Unique
Identifier (GUID), the Locally Unique Identifier (LUID), and the
Security Identifier (SID) — and the Security Descriptor (SD) family.

This document covers:

- GUID — binary format, string representation, comparison semantics,
  and generation requirements
- LUID — binary format, comparison semantics, and allocation model
- SID — binary format, string representation, comparison
  semantics, and the well-known SID catalogue
- SD — the security descriptor structure and its subtypes: ACL and
  ACE formats, access masks, ACE ordering, inheritance, ownership,
  conditional ACEs and their bytecode, claim attributes, and
  resource attributes

This document does not cover:

- Well-known GUID and LUID values — defined in the specifications
  of the subsystems that declare them
- SID-bearing aggregate structures such as SID_AND_ATTRIBUTES —
  described in the Peios Kernel TRM §3.2.2
- The access-check algorithm that evaluates these structures —
  described in the Peios Kernel TRM §3.8
- Per-object-type SD storage locations — described in the Peios
  Kernel TRM §3.3.3 for processes and §3.9.5 for files
- Application-specific identifier namespaces

Three of those exclusions point at a manual rather than at another
specification, which is deliberate and worth stating once. KACS — the
kernel's access-control implementation — is described in the Peios
Kernel TRM §3 and is not separately specified. Peios does not offer a
standard from which a second, independent access-control implementation
could be built; it offers a manual describing the one that exists.

What a third party does need is the other half: the structures that
cross the boundary into that implementation, and the rules for reading
and writing them. That is this document, and it is specified. Where a
structure's meaning depends on what KACS does with it, this document
names the manual article that describes the behaviour rather than
restating it (Conventions §4.7).

---

# 1.2 Conventions

_Peios / Advanced Peios / PCDS / Introduction_

> The normative keywords PCDS uses, and the shared conventions it inherits from the PCSA conventions book.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this
document are to be interpreted as described in RFC 2119. Text set off as
a note is informative, not normative.

Everything else — roles, byte order, sizes, layout tables, notation,
strings, timestamps, citation, and the external standards this anthology
depends on — is defined in the Conventions book and is not restated
here. PCDS departs from none of it.

Where a chapter needs a convention of its own, that chapter states it.

---

# 2.1 Binary Format

_Peios / Advanced Peios / PCDS / GUID_

> The 128-bit GUID — its four-field binary layout, byte order, and the nil GUID sentinel.

A GUID (Globally Unique Identifier) is a 128-bit identifier with
global uniqueness guarantees, used to identify registry hives,
layers, object types, and other entities that require stable
identity across systems and reboots. The GUID with all 128 bits set
to zero is the **nil GUID**, a sentinel value meaning "no GUID" or
"unset."

A GUID is a 128-bit (16-byte) value with the following binary layout:

| Offset | Size | Field | Type |
|--------|------|-------|------|
| 0 | 4 | Data1 | uint32, little-endian |
| 4 | 2 | Data2 | uint16, little-endian |
| 6 | 2 | Data3 | uint16, little-endian |
| 8 | 8 | Data4 | uint8[8] |

The total size of a GUID MUST be exactly 16 bytes with no
padding.

Data1, Data2, and Data3 MUST be stored in little-endian byte
order.

Data4 is a raw byte array with no endianness interpretation.

> [!NOTE]
> This is the mixed-endian layout inherited from DCE RPC and
> MS-DTYP. The first three fields follow the platform's native byte
> order (little-endian on x86), while Data4 is a byte sequence with
> no integer interpretation.

## 2.1.1 Nil GUID

The nil GUID is the GUID with all 16 bytes set to zero.

The nil GUID is a valid GUID value. Specifications that require a
non-nil GUID MUST state this requirement explicitly.

The nil GUID MUST NOT be produced by the generation algorithm
defined in §2.4.

---

# 2.2 String Format

_Peios / Advanced Peios / PCDS / GUID_

> The braced 8-4-4-4-12 hex form of a GUID, its exact 38-character length, lowercase output, and case-insensitive parsing.

## 2.2.1 Canonical form

The canonical string representation of a GUID MUST use the following
format:

```
{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
```

The string MUST be exactly 38 characters: an opening brace, 32 hex
digits arranged in 8-4-4-4-12 groups separated by hyphens, and a
closing brace.

The fields map to the string as follows:

| Group | Digits | Source |
|-------|--------|--------|
| 1 | 8 | Data1, most significant nibble first |
| 2 | 4 | Data2, most significant nibble first |
| 3 | 4 | Data3, most significant nibble first |
| 4 | 4 | Data4[0] and Data4[1], in byte order |
| 5 | 12 | Data4[2] through Data4[7], in byte order |

For Data1, Data2, and Data3, the hex representation is of the
numeric value (most significant nibble first), not of the stored
byte order. For Data4, each byte is encoded in sequence with the
high nibble before the low nibble.

> [!NOTE]
> Example. Given the 16 bytes (in storage order):
>
> ```
> 04 03 02 01  06 05  08 07  09 0a  0b 0c 0d 0e 0f 10
> ```
>
> Data1 = 0x01020304 (little-endian), Data2 = 0x0506, Data3 = 0x0708.
> The string representation is:
>
> ```
> {01020304-0506-0708-090a-0b0c0d0e0f10}
> ```

## 2.2.2 Case

Canonical output MUST use lowercase hex digits (`a`–`f`).

## 2.2.3 Parsing

Parsers MUST accept both uppercase and lowercase hex digits.

Parsers MUST accept the braced form `{...}` and SHOULD accept the
unbraced form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.

Parsers MUST reject strings that do not have exactly the right
number of hex digits in each group.

---

# 2.3 Comparison

_Peios / Advanced Peios / PCDS / GUID_

> GUID equality is byte-for-byte on the binary form; no total ordering is defined, and a specification that needs one must define its own.

## 2.3.1 Equality

Two GUIDs are equal if and only if their 16-byte binary
representations are identical.

GUID comparison MUST be performed on the binary representation,
not on string representations.

> [!NOTE]
> Byte-for-byte comparison of the binary form avoids case
> sensitivity and brace-presence issues that would arise from
> comparing string representations.

## 2.3.2 Ordering

This document does not define a total ordering for GUIDs.
Specifications that require an ordered GUID collection MUST define
their ordering convention explicitly.

---

# 2.4 Generation

_Peios / Advanced Peios / PCDS / GUID_

> Peios GUIDs are RFC 4122 version 4 — filled from a cryptographic random source, with the version and variant bits fixed afterwards.

GUIDs generated by Peios MUST be version 4 (random) as defined in
RFC 4122 §4.4.

## 2.4.1 Algorithm

To generate a version 4 GUID:

1. Fill all 16 bytes with cryptographically random data.
2. Set the four most significant bits of Data3 to `0100`
   (version 4).
3. Set the two most significant bits of Data4[0] to `10`
   (RFC 4122 variant).

The resulting GUID has 122 random bits, 4 version bits, and
2 variant bits.

> [!NOTE]
> In terms of bit manipulation on the binary layout:
>
> ```
> Data3 = (Data3 & 0x0fff) | 0x4000
> Data4[0] = (Data4[0] & 0x3f) | 0x80
> ```

## 2.4.2 Randomness source

The random data MUST be obtained from a cryptographically secure
source.

In the kernel, this MUST be `get_random_bytes()` or equivalent.

In userspace, this MUST be `getrandom(2)` with no flags (blocking
until the entropy pool is initialised) or equivalent.

GUIDs MUST NOT be generated using a pseudorandom number generator
seeded from a predictable source.

---

# Appendix 2.A Prior Art

_Peios / Advanced Peios / PCDS / GUID_

> Where the Peios GUID comes from — MS-DTYP §2.3.4, RFC 4122 and DCE RPC — and the one place Peios normalises differently.

## 2.A.1 MS-DTYP

The GUID type defined in this document derives from the Microsoft
Data Types specification (MS-DTYP), §2.3.4. The Peios GUID binary
format is identical to the MS-DTYP GUID. The Peios GUID string
format follows the same hyphenated hex convention but normalises to
lowercase hex digits on output, where Microsoft implementations
typically produce uppercase.

## 2.A.2 RFC 4122

RFC 4122 ("A Universally Unique IDentifier (UUID) URN Namespace")
defines the UUID format and generation algorithms. The GUID
binary layout used by Microsoft and adopted by Peios is the
mixed-endian variant of the RFC 4122 UUID: the first three fields
are little-endian integers and the last field is a raw byte array.
This differs from the RFC 4122 network byte order representation
where all fields are big-endian.

Peios generates version 4 (random) GUIDs as defined in RFC 4122 §4.4.

## 2.A.3 DCE RPC

The GUID structure originates from the DCE 1.1 RPC specification,
which defined the `uuid_t` type with the same field layout. The
mixed-endian encoding reflects the DCE convention of encoding integer
fields in the sender's native byte order (little-endian on x86).

---

# 3.1 Binary Format

_Peios / Advanced Peios / PCDS / LUID_

> The 64-bit LUID — LowPart and HighPart, its boot-scoped uniqueness, and the nil LUID.

An LUID (Locally Unique Identifier) is a 64-bit identifier with
boot-scoped local uniqueness, used to identify transient entities
such as logon sessions and privilege instances that do not persist
across reboots. The LUID with all 64 bits set to zero is the
**nil LUID**, a sentinel value meaning "no LUID" or "unset."

An LUID is a 64-bit (8-byte) value with the following binary layout:

| Offset | Size | Field | Type |
|--------|------|-------|------|
| 0 | 4 | LowPart | uint32, little-endian |
| 4 | 4 | HighPart | uint32, little-endian |

The total size of an LUID MUST be exactly 8 bytes with no
padding.

Both fields MUST be stored in little-endian byte order.

> [!NOTE]
> MS-DTYP defines HighPart as a signed 32-bit integer (LONG). Peios
> uses unsigned uint32 for both fields. The signed type in MS-DTYP
> is a Win32 API convention with no semantic purpose — LUID values
> are never negative. See §3.A for the full divergence rationale.

## 3.1.1 Nil LUID

The nil LUID is the LUID with all 8 bytes set to zero
(LowPart = 0, HighPart = 0).

The nil LUID is a valid LUID value. The nil LUID MUST NOT be
assigned by the allocation algorithm defined in §3.3.

Specifications that require a non-nil LUID MUST state this
requirement explicitly.

---

# 3.2 Comparison

_Peios / Advanced Peios / PCDS / LUID_

> LUID equality compares both halves, and monotonic allocation within a boot must not be read as a temporal ordering across sources.

## 3.2.1 Equality

Two LUIDs are equal if and only if both their LowPart and HighPart
fields are identical.

## 3.2.2 Ordering

This document does not define a total ordering for LUIDs.
Although LUIDs are allocated monotonically within a boot session
(see §3.3), consumers MUST NOT rely on numeric ordering to infer
temporal relationships between LUIDs obtained from different
contexts.

---

# 3.3 Allocation

_Peios / Advanced Peios / PCDS / LUID_

> LUIDs are kernel-allocated, unique within a boot session, strictly monotonic, and never fabricated by userspace.

LUIDs MUST be allocated by the kernel.

## 3.3.1 Uniqueness scope

Each LUID MUST be unique within a single boot session.

LUID values MUST NOT be assumed unique across reboots. A value
allocated in one boot session MAY be reused in a subsequent boot
session.

## 3.3.2 Monotonicity

The kernel MUST allocate LUIDs in strictly monotonically increasing
order within a boot session, treating the two fields as a single
unsigned 64-bit integer (HighPart << 32 | LowPart).

The starting value of the allocation sequence after each boot is
implementation-defined.

> [!NOTE]
> Subsystems that define well-known LUID values (such as privilege
> identifiers) typically reserve values below the allocation starting
> point. The starting value should be chosen to leave room for
> current and future well-known values.

## 3.3.3 Fabrication prohibition

Userspace code MUST NOT fabricate LUID values. All LUIDs MUST be
obtained through the kernel allocation interface or from well-known
constants defined in a Peios specification.

---

# Appendix 3.A Prior Art

_Peios / Advanced Peios / PCDS / LUID_

> The LUID derives from MS-DTYP §2.3.7, diverging in one detail — HighPart is unsigned where MS-DTYP has it signed.

## 3.A.1 MS-DTYP

The LUID type defined in this document derives from the Microsoft
Data Types specification (MS-DTYP), §2.3.7.

The Peios LUID binary format diverges from MS-DTYP in one detail:
HighPart is an unsigned 32-bit integer (uint32) rather than the
signed 32-bit integer (LONG) used in MS-DTYP. The signed type in
MS-DTYP is a Win32 API convention with no semantic purpose — LUID
values are never negative. Making the field unsigned simplifies
comparison and eliminates a class of sign-extension bugs.

---

# 4.1 Binary Format

_Peios / Advanced Peios / PCDS / SID_

> The variable-length SID — revision, identifier authority, and up to fifteen sub-authorities, with the size limits that bound it.

A SID (Security Identifier) is a variable-length binary value that
uniquely identifies a principal — a user, group, service, machine, or
well-known entity. SIDs are the fundamental identity primitive of the
Peios security model: they appear in tokens as identity, in security
descriptors as access rules, and as references throughout the system.

A SID is encoded as a contiguous binary structure with the following
layout:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | Revision | MUST be 1. |
| 1 | 1 | SubAuthorityCount | Number of sub-authorities. MUST be between 0 and 15 inclusive. |
| 2 | 6 | IdentifierAuthority | A 6-byte big-endian value identifying the authority that issued the SID. |
| 8 | 4 × SubAuthorityCount | SubAuthority[] | Array of 32-bit unsigned integers in little-endian byte order. |

The total size of a SID in bytes is `8 + (4 × SubAuthorityCount)`.
The minimum size is 8 bytes (zero sub-authorities). The maximum size
is 68 bytes (15 sub-authorities).

The last sub-authority in a SID is the **Relative Identifier (RID)**
— the portion that distinguishes individual principals within a
domain.

> [!NOTE]
> The IdentifierAuthority is the one big-endian field among the
> structures in this document — an MS-DTYP inheritance. The
> sub-authorities that follow it are ordinary little-endian integers.

---

# 4.2 String Format

_Peios / Advanced Peios / PCDS / SID_

> The S-R-I-S string form of a SID — how each component is rendered, and the rules for parsing one back.

SIDs are represented in string form as:

```
S-1-{authority}-{sub1}-{sub2}-...-{subN}
```

Where:

- `S` is a literal prefix.
- `1` is the revision number.
- `{authority}` is the IdentifierAuthority. If the upper 2 bytes are
  zero, this is the decimal representation of the lower 4 bytes.
  Otherwise, it is the lowercase hexadecimal representation of all
  6 bytes, zero-padded to 12 hex digits and prefixed with `0x`.
- Each `{subN}` is the decimal representation of the corresponding
  32-bit sub-authority.

> [!NOTE]
> Example SIDs: `S-1-5-18` (Local System), `S-1-5-32-544`
> (BUILTIN\Administrators),
> `S-1-5-21-3623811015-3361044348-30300820-1013` (a domain user).
> The meanings of well-known SID values are catalogued in §4.4.

---

# 4.3 Comparison

_Peios / Advanced Peios / PCDS / SID_

> SID equality is an exact binary match — no case folding, no normalisation, no equivalence relation — and no total ordering is defined.

## 4.3.1 Equality

Two SIDs are equal if and only if their binary representations are
byte-for-byte identical.

SID comparison MUST be performed on the binary encoding, not the
string form. There is no case sensitivity, normalisation, or
equivalence relation — equality is exact binary match.

## 4.3.2 Ordering

This document does not define a total ordering for SIDs.
Specifications that require an ordered SID collection MUST define
their ordering convention explicitly.

---

# 4.4 Well-Known SIDs

_Peios / Advanced Peios / PCDS / SID_

> The fixed SIDs every implementation must recognise — universal and creator authorities, NT Authority, BUILTIN, integrity and trust labels, confinement and capability SIDs.

The following SIDs have fixed values and well-defined meanings. An
implementation MUST recognise these SIDs and apply their defined
semantics wherever they are referenced. The access-check behaviour
attached to them is described in the Peios Kernel TRM §3.8.

## 4.4.1 Universal authorities

| SID | Name | Description |
|---|---|---|
| S-1-0-0 | Nobody | The null SID. No principal. |
| S-1-1-0 | Everyone | Matches all principals, including anonymous. |
| S-1-2-0 | Local | Principals that log on locally (physically). |
| S-1-2-1 | Console Logon | Principals that log on via the physical console. |

## 4.4.2 Creator authorities

| SID | Name | Description |
|---|---|---|
| S-1-3-0 | Creator Owner | Placeholder in inheritable ACEs. Replaced with the creating principal's SID during inheritance. |
| S-1-3-1 | Creator Group | Placeholder in inheritable ACEs. Replaced with the creating principal's primary group SID during inheritance. |
| S-1-3-4 | Owner Rights | When present in a DACL, overrides the owner's implicit READ_CONTROL and WRITE_DAC grants. AccessCheck treats this SID as matching the object's owner. |

## 4.4.3 NT Authority (S-1-5)

| SID | Name | Description |
|---|---|---|
| S-1-5-2 | Network | Principals that authenticated over the network. |
| S-1-5-4 | Interactive | Principals that logged on interactively. |
| S-1-5-6 | Service | Principals that authenticated as a service. |
| S-1-5-7 | Anonymous | The anonymous identity. Carried by tokens at Anonymous impersonation level. |
| S-1-5-10 | Principal Self | Placeholder in ACEs on directory objects. Matches the caller when the caller's identity corresponds to the object's associated principal. Resolved via the `self_sid` parameter to AccessCheck. |
| S-1-5-11 | Authenticated Users | All principals that have been authenticated (excludes Anonymous). |
| S-1-5-18 | Local System (SYSTEM) | The operating system's own identity. Highest privilege level. |
| S-1-5-19 | Local Service | A built-in service account with reduced privileges. |
| S-1-5-20 | Network Service | A built-in service account that can authenticate to remote services. |

## 4.4.4 Logon SIDs

| SID | Name | Description |
|---|---|---|
| S-1-5-5-*X*-*Y* | Logon SID | A per-authentication-event SID generated at LogonSession creation. *X* and *Y* are unique values. Injected into the token's groups with SE_GROUP_LOGON_ID. |

## 4.4.5 BUILTIN domain (S-1-5-32)

| SID | Name | Description |
|---|---|---|
| S-1-5-32-544 | BUILTIN\Administrators | The built-in administrators group. |
| S-1-5-32-545 | BUILTIN\Users | The built-in users group. |
| S-1-5-32-546 | BUILTIN\Guests | The built-in guests group. |
| S-1-5-32-551 | BUILTIN\Backup Operators | Members can bypass file security for backup and restore. |

> [!NOTE]
> Additional BUILTIN SIDs (S-1-5-32-547 through S-1-5-32-583) are
> defined by Active Directory. KACS does not assign special semantics
> to these SIDs — they participate in normal ACE matching like any
> other group SID. Their meaning is an administrative convention, not
> a kernel enforcement property.

## 4.4.6 Domain SIDs (S-1-5-21)

Domain-specific SIDs follow the pattern
`S-1-5-21-{DA1}-{DA2}-{DA3}-{RID}`, where the three domain authority
sub-authorities identify the domain and the RID identifies the
principal within that domain.

| RID | Name | Description |
|---|---|---|
| 500 | Domain Administrator | The built-in administrator account. |
| 501 | Domain Guest | The built-in guest account. |
| 512 | Domain Admins | The domain administrators group. |
| 513 | Domain Users | The domain users group. |
| 514 | Domain Guests | The domain guests group. |
| 515 | Domain Computers | Computer accounts in the domain. |

> [!NOTE]
> Domain SIDs are assigned by the domain controller and replicated
> through Active Directory. KACS does not create or manage domain
> SIDs — it evaluates them as opaque binary values during
> AccessCheck.

## 4.4.7 Mandatory integrity labels (S-1-16)

| SID | Name | Numeric level | Description |
|---|---|---|---|
| S-1-16-0 | Untrusted | 0 | Lowest trust. Sandboxed or experimental code. |
| S-1-16-4096 | Low | 4096 | Reduced trust. Services handling untrusted input. |
| S-1-16-8192 | Medium | 8192 | Standard trust. Default for interactive logons and most services. |
| S-1-16-12288 | High | 12288 | Elevated administrative logons. |
| S-1-16-16384 | System | 16384 | The kernel, peinit, and TCB services. |

The five levels above are the standard, well-known integrity levels;
in practice they behave like an enum. Technically the level is the
SID's single sub-authority as an unsigned integer: any `S-1-16-<n>`
with exactly one sub-authority is a valid level, and MIC compares
levels numerically. Non-standard values occur in Windows-interop SDs
— e.g. `S-1-16-8448` (medium-plus) or `S-1-16-20480` (protected). A
mandatory-label SID with a different identifier authority or more
than one sub-authority is malformed and rejected. The standard order
is System > High > Medium > Low > Untrusted.

## 4.4.8 Process trust labels (S-1-19)

| SID | Name | Description |
|---|---|---|
| S-1-19-0-0 | None / No trust | Default for unsigned processes. |
| S-1-19-512-1024 | Protected, Authenticode | Third-party signed binaries. |
| S-1-19-512-1536 | Protected, AntiMalware | Security tooling. |
| S-1-19-512-2048 | Protected, App | Peios-distributed applications. |
| S-1-19-512-4096 | Protected, Peios | Core Peios components. |
| S-1-19-512-8192 | Protected, PeiosTcb | Peios Trusted Computing Base. |
| S-1-19-1024-8192 | Isolated, PeiosTcb | Maximum isolation and trust. |

Trust labels encode two dimensions in the SID: the first
sub-authority is the PIP type axis and the second is the trust axis
(higher = more trusted). Dominance requires both dimensions to be
greater than or equal.

KACS currently standardises these PIP type values:

- `0` = None
- `512` = Protected
- `1024` = Isolated

These values are standardised labels, not a closed enum for
AccessCheck. Other numeric type values remain valid and are compared
numerically by the same dominance rule.

## 4.4.9 Confinement SIDs (S-1-15)

| SID | Name | Description |
|---|---|---|
| S-1-15-2-*hash* | Confinement SID | Identifies a confined application. The sub-authorities are derived from the application identity. |
| S-1-15-2-1 | ALL_APPLICATION_PACKAGES | Matches all confined applications in normal confinement mode. |
| S-1-15-2-2 | ALL_RESTRICTED_APPLICATION_PACKAGES | Matches confined applications in both normal and strict confinement modes. Strict confinement is the mode where ALL_APPLICATION_PACKAGES is omitted from the capabilities. |

## 4.4.10 Capability SIDs (S-1-15-3)

| SID | Name | Description |
|---|---|---|
| S-1-15-3-1 | internetClient | Outbound internet access. |
| S-1-15-3-2 | internetClientServer | Inbound and outbound internet access. |
| S-1-15-3-3 | privateNetworkClientServer | LAN/private network access. |
| S-1-15-3-8 | enterpriseAuthentication | Domain credential access. |
| S-1-15-3-9 | sharedUserCertificates | Certificate store access. |
| S-1-15-3-10 | removableStorage | Removable media access. |

Capability SIDs 4–7 (picturesLibrary, videosLibrary, musicLibrary,
documentsLibrary) are reserved. Their SID values MUST NOT be
redefined.

Derived capabilities use 8 sub-authorities computed from the SHA-256
hash of the capability name:
`S-1-15-3-{h0}-{h1}-{h2}-{h3}-{h4}-{h5}-{h6}-{h7}`. The same name
always produces the same SID.

## 4.4.11 Service SIDs

Service SIDs follow the pattern `SERVICE\{service_name}` (e.g.,
`SERVICE\jellyfin`, `SERVICE\loregd`) and are added as a group in
the service's token. The token's primary user SID is the account the
service runs as (typically SYSTEM, LocalService, or NetworkService);
the service SID enables per-service access control — a file's DACL
can grant access to `SERVICE\jellyfin` specifically, rather than to
the broad account the service runs under.

The SID value is derived from the service name using a SHA-1 hash:
the UTF-16LE encoding of the uppercased service name is hashed, and
the 20-byte digest is split into five little-endian 32-bit
sub-authorities: `S-1-5-80-{h0}-{h1}-{h2}-{h3}-{h4}`. The same
service name always produces the same SID. This matches the Windows
service SID derivation (MS-DTYP compatible).

---

# Appendix 4.A Prior Art

_Peios / Advanced Peios / PCDS / SID_

> The SID derives from MS-DTYP §2.4.2, keeping both the packet encoding and the S-R-I-S string convention.

## 4.A.1 MS-DTYP

The SID type defined in this document derives from the Microsoft
Data Types specification (MS-DTYP), §2.4.2. The Peios binary
encoding is identical to the MS-DTYP packet representation, and the
string form follows the same `S-R-I-S` convention. The structure
itself originates in Windows NT, where it has been the principal
identity primitive since NT 3.1.

---

# 5.1 SD Structure

_Peios / Advanced Peios / PCDS / Security Descriptor_

> The Security Descriptor — owner, group, DACL and SACL, the self-relative binary layout, the sixteen control flags, and null DACL against empty DACL.

A Security Descriptor (SD) defines the complete security policy for a protected object. Every protected object in Peios — every file, registry key, IPC endpoint, service, token, and process — MUST have a Security Descriptor.

An SD has four components and a set of control flags:

- **Owner SID** — the principal that owns the object. The owner has implicit rights (READ_CONTROL and WRITE_DAC) unless suppressed by an OWNER RIGHTS ACE.

- **Group SID** — an optional primary group associated with the object. When present, it is stored and returned on query and used during CREATOR GROUP substitution during inheritance. When absent, no primary group is available for those metadata operations. No access control decision depends on the group SID directly. If an inheritance or ACL-rewrite operation must materialize a CREATOR GROUP SID and the source object SD has no group SID, KACS MUST fail closed rather than substitute another SID or silently drop the ACE.

- **DACL** (Discretionary Access Control List) — an ordered list of ACEs that define who is allowed or denied access. The object's owner controls the DACL (via WRITE_DAC).

- **SACL** (System Access Control List) — an ordered list of ACEs that define system-level policy. Despite the name, the SACL carries several distinct ACE types:
  - Audit ACEs — which access attempts to log.
  - Mandatory label ACE — the object's integrity level for MIC.
  - Resource attribute ACEs — name-value attributes for conditional ACE evaluation.
  - Scoped policy ID ACEs — references to central access policies.
  - Process trust label ACE — the object's PIP trust level.

  Modifying the SACL requires ACCESS_SYSTEM_SECURITY. ACCESS_SYSTEM_SECURITY is privilege-controlled: it is normally granted by SeSecurityPrivilege, and may also be granted by restore-intent SeRestorePrivilege in the `kacs_set_sd` cases described in the Peios Kernel TRM §3.9.6.

Both the DACL and SACL use the standard binary ACL format defined in §5.2.

## 5.1.1 Binary format

SDs use the self-relative binary format defined in MS-DTYP §2.4.6. The format is a 20-byte header followed by the owner SID, optional group SID, SACL, and DACL at offsets specified in the header.

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | `Revision` | MUST be 1. |
| 1 | 1 | `Sbz1` | Reserved. Preserved for format compatibility but not interpreted by KACS. When SE_RM_CONTROL_VALID is set in the control flags, this byte carries resource manager control bits defined by the originating system. |
| 2 | 2 | `Control` | Control flags, little-endian. |
| 4 | 4 | `OffsetOwner` | Offset to the owner SID, little-endian. 0 if absent. |
| 8 | 4 | `OffsetGroup` | Offset to the group SID, little-endian. 0 if absent. |
| 12 | 4 | `OffsetSacl` | Offset to the SACL, little-endian. 0 if absent. |
| 16 | 4 | `OffsetDacl` | Offset to the DACL, little-endian. 0 if absent. |

Field names are those of MS-DTYP §2.4.6.

The self-relative format packs everything into a contiguous byte buffer with no pointers. This makes it suitable for storage (xattrs, database fields) and wire transmission (IPC, SMB).

KACS MUST use the self-relative format exclusively.

## 5.1.2 Control flags

The SD's 16-bit Control field records metadata about the descriptor:

| Flag | Bit | Value | Description |
|---|---|---|---|
| SE_OWNER_DEFAULTED (OD) | 0 | 0x0001 | The owner was established by default means. |
| SE_GROUP_DEFAULTED (GD) | 1 | 0x0002 | The group was established by default means. |
| SE_DACL_PRESENT (DP) | 2 | 0x0004 | A DACL is present. If clear, AccessCheck treats the DACL as null (grants all access). |
| SE_DACL_DEFAULTED (DD) | 3 | 0x0008 | The DACL was established by default means. |
| SE_SACL_PRESENT (SP) | 4 | 0x0010 | A SACL is present. |
| SE_SACL_DEFAULTED (SD) | 5 | 0x0020 | The SACL was established by default means. |
| SE_DACL_TRUSTED (DT) | 6 | 0x0040 | Reserved metadata. Preserved during round-trip serialisation. No operational semantics. |
| SE_SERVER_SECURITY (SS) | 7 | 0x0080 | Create a server ACL based on the input ACL. |
| SE_DACL_AUTO_INHERIT_REQ (AR) | 8 | 0x0100 | Requests that the DACL be auto-inherited from the parent. When clear on a creator SD, parent inheritance is suppressed even if SE_DACL_PROTECTED is not set. |
| SE_SACL_AUTO_INHERIT_REQ | 9 | 0x0200 | Requests that the SACL be auto-inherited from the parent. Same semantics as bit 8 for the SACL. |
| SE_DACL_AUTO_INHERITED (DI) | 10 | 0x0400 | The DACL was created through automatic inheritance. |
| SE_SACL_AUTO_INHERITED (SI) | 11 | 0x0800 | The SACL was created through automatic inheritance. |
| SE_DACL_PROTECTED (PD) | 12 | 0x1000 | The DACL is protected from inheritance. Inheritable ACEs from parent objects MUST NOT be merged. |
| SE_SACL_PROTECTED (PS) | 13 | 0x2000 | The SACL is protected from inheritance. |
| SE_RM_CONTROL_VALID (RM) | 14 | 0x4000 | The Sbz1 byte is interpreted as resource manager control bits. |
| SE_SELF_RELATIVE (SR) | 15 | 0x8000 | The SD is in self-relative format. MUST always be set for stored SDs. |

The architectural maximum SD size is 65,535 bytes. KACS MUST reject any parsed or computed SD whose serialised self-relative byte length exceeds 65,535 bytes.

The DEFAULTED flags are metadata. During object-SD creation, KACS MUST set the corresponding DEFAULTED flag when it supplies that component from a default source rather than from an explicit creator SD component or inherited ACL. KACS MUST NOT grant or deny access based solely on a DEFAULTED flag.

This document records the `SE_SERVER_SECURITY` flag value but does not
define the server-ACL construction algorithm. An implementation MUST
fail closed when a creator SD attempts to use `SE_SERVER_SECURITY`.

The PROTECTED flags are operationally significant. Setting SE_DACL_PROTECTED prevents inheritance from parent objects — the object keeps its current ACEs and stops accepting new inheritable ACEs from above.

## 5.1.3 Null DACL vs empty DACL

The SE_DACL_PRESENT flag distinguishes two states with very different security consequences:

- **Null DACL** (SE_DACL_PRESENT not set) — no discretionary access control. AccessCheck grants all requested access to every caller. This SHOULD almost never be used.

- **Empty DACL** (SE_DACL_PRESENT set, zero ACEs) — AccessCheck grants no access to any caller (except the owner's implicit rights). An explicit statement that no principal has discretionary access.

Objects SHOULD always have a DACL. This is a preferred-object-shape recommendation, not a fail-closed requirement. If object creation has no explicit DACL, no inherited DACL, and the creator token has no default DACL, the resulting object SD has a null DACL: SE_DACL_PRESENT is clear and the DACL offset is zero.

---

# 5.2 ACL Format

_Peios / Advanced Peios / PCDS / Security Descriptor_

> The 8-byte ACL header and the contiguous ACE array it introduces, with the rules for parsing a self-delimiting ACE sequence.

An Access Control List (ACL) is the binary container for ACEs. DACLs, SACLs, and CAAP policy ACL blobs all use the same standard binary ACL format.

## 5.2.1 Binary format

An ACL begins with an 8-byte header followed by `AceCount` ACEs packed contiguously:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | AclRevision | ACL revision number. Determines which ACE families the ACL formally permits. |
| 1 | 1 | Sbz1 | Reserved. Preserved for compatibility but not interpreted by KACS. |
| 2 | 2 | AclSize | Total size of the ACL in bytes, including the 8-byte header. Little-endian. |
| 4 | 2 | AceCount | Number of ACEs in the ACL. Little-endian. |
| 6 | 2 | Sbz2 | Reserved. Preserved for compatibility but not interpreted by KACS. |

The ACE array begins immediately at offset 8. Each ACE is self-delimiting via its `AceSize` field. The parser walks the ACL by iterating exactly `AceCount` ACEs within the `AclSize` boundary.

## 5.2.2 Parsing rules

- `AclSize` MUST be at least 8 bytes.
- `AclSize` MUST NOT exceed the containing buffer.
- The ACL body MUST contain exactly `AceCount` ACEs within the declared `AclSize`.
- Truncated ACEs, ACE overruns, or leftover bytes within `AclSize` are malformed.
- The architectural maximum ACL size is 64 KB because `AclSize` is a 16-bit field.

ACE structure, ACE-type definitions, and revision-versus-ACE-family rules are specified in §5.4.

---

# 5.3 Access Masks

_Peios / Advanced Peios / PCDS / Security Descriptor_

> The 32-bit access mask — its object-specific, standard, special and generic right regions, the reserved bits, and generic mapping.

Every ACE carries an access mask — a 32-bit integer where each bit represents a specific right. The same 32-bit layout is used in three contexts: the ACE's mask (what the rule grants or denies), the requested access (what the caller asks for), and the granted access (what AccessCheck returns).

## 5.3.1 Bit layout

The 32 bits are divided into four regions:

### 5.3.1.1 Object-specific rights (bits 0–15)

Defined by the object type. Different object types assign different meanings to these bits. A file uses bits for read, write, append, execute; a registry key uses bits for query value, set value, create subkey; a token uses bits for query, duplicate, impersonate. Each subsystem defines its own mapping.

### 5.3.1.2 Standard rights (bits 16–20)

Common to all object types:

| Bit | Name | Value | Meaning |
|---|---|---|---|
| 16 | DELETE | 0x00010000 | Delete the object. |
| 17 | READ_CONTROL | 0x00020000 | Read the object's SD (excluding SACL). |
| 18 | WRITE_DAC | 0x00040000 | Modify the object's DACL. |
| 19 | WRITE_OWNER | 0x00080000 | Change the object's owner. |
| 20 | SYNCHRONIZE | 0x00100000 | Wait on the object. |

### 5.3.1.3 Special rights (bits 24–25)

| Bit | Name | Value | Meaning |
|---|---|---|---|
| 24 | ACCESS_SYSTEM_SECURITY | 0x01000000 | Read or write the SACL. Requires SeSecurityPrivilege. |
| 25 | MAXIMUM_ALLOWED | 0x02000000 | Not a real right. Request flag that tells AccessCheck to compute and return the maximum set of rights the caller would be granted. MUST NOT appear in an ACE. |

### 5.3.1.4 Generic rights (bits 28–31)

Abstract rights mapped to object-specific rights before evaluation:

| Bit | Name | Value |
|---|---|---|
| 28 | GENERIC_ALL | 0x10000000 |
| 29 | GENERIC_EXECUTE | 0x20000000 |
| 30 | GENERIC_WRITE | 0x40000000 |
| 31 | GENERIC_READ | 0x80000000 |

### 5.3.1.5 Reserved bits

Bits 21–23 and 26–27 are reserved and MUST NOT be used. An access mask
setting any of them is rejected: a desired access mask carrying one
fails the request, and an ACE mask carrying one makes the containing SD
unparseable.

## 5.3.2 Generic mapping

Generic rights exist because SDs need to be portable across object types. A central access policy might say "allow GENERIC_READ on all objects" — and GENERIC_READ means different specific bits for files versus registry keys.

Each object type defines a **GenericMapping** table:

| Field | Description |
|---|---|
| `read` | Specific + standard bits that GENERIC_READ maps to. |
| `write` | Specific + standard bits that GENERIC_WRITE maps to. |
| `execute` | Specific + standard bits that GENERIC_EXECUTE maps to. |
| `all` | Specific + standard bits that GENERIC_ALL maps to. |

Generic mapping happens once, at request time. AccessCheck MUST map any generic bits in the desired mask to object-specific bits using the object type's GenericMapping table, then clear the generic bits. The DACL walk operates exclusively on specific and standard bits.

> [!NOTE]
> KACS maps ACE masks via GenericMapping at evaluation time (using a local variable — the ACE itself is never mutated). This is an intentional divergence: the reference model expects ACE masks to be pre-mapped at SD construction time. Defensive mapping at evaluation time handles two cases: (1) imported/external SDs that may contain unresolved generic bits, and (2) central access policy recovery ACEs that use GENERIC_ALL. Note: SD inheritance also maps generic bits on all ACEs when computing a child SD (see §5.6), so newly created SDs will not contain generic bits. The evaluation-time mapping is defense-in-depth for SDs from other sources.

---

# 5.4 ACE Types

_Peios / Advanced Peios / PCDS / Security Descriptor_

> Every ACE type and body layout — the single-SID, object, callback and resource-attribute families, the AceType constants, and the ACL revision rules.

An Access Control Entry (ACE) is a single rule in an ACL. Each ACE has a header, an access mask, and a principal SID, with optional extensions for object-type and conditional ACEs.

## 5.4.1 ACE header

Every ACE begins with a 4-byte header:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | AceType | Identifies the ACE type. |
| 1 | 1 | AceFlags | Inheritance and audit flags. |
| 2 | 2 | AceSize | Total size of the ACE in bytes, including the header. MUST be a multiple of 4. |

## 5.4.2 ACE body layouts

The ACE header is followed by a type-specific body. Every multibyte integer in
the body is little-endian.

### 5.4.2.1 Single-SID ACE family

The following ACE types share the same binary layout:

- `ACCESS_ALLOWED_ACE`
- `ACCESS_DENIED_ACE`
- `SYSTEM_AUDIT_ACE`
- `SYSTEM_ALARM_ACE`
- `SYSTEM_MANDATORY_LABEL_ACE`
- `SYSTEM_SCOPED_POLICY_ID_ACE`
- `SYSTEM_PROCESS_TRUST_LABEL_ACE`

Layout:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | AceHeader | Standard ACE header. |
| 4 | 4 | Mask | Access mask. |
| 8 | variable | Sid | Principal SID. Consumes the remainder of the ACE. |

Parsing rules:

- `AceSize` MUST be at least 16 bytes (header + mask + minimum SID).
- The SID MUST consume the remainder of the ACE exactly.

### 5.4.2.2 Object ACE family

The following ACE types share the object-ACE binary layout:

- `ACCESS_ALLOWED_OBJECT_ACE`
- `ACCESS_DENIED_OBJECT_ACE`
- `SYSTEM_AUDIT_OBJECT_ACE`
- `SYSTEM_ALARM_OBJECT_ACE`

Layout:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | AceHeader | Standard ACE header. |
| 4 | 4 | Mask | Access mask. |
| 8 | 4 | Flags | Bitfield describing which GUIDs are present. |
| 12 | 0 or 16 | ObjectType | Present when `ACE_OBJECT_TYPE_PRESENT` is set. |
| 12 or 28 | 0 or 16 | InheritedObjectType | Present when `ACE_INHERITED_OBJECT_TYPE_PRESENT` is set. |
| variable | variable | Sid | Principal SID. Begins immediately after the optional GUID fields and consumes the remainder of the ACE. |

Object ACE flags:

| Flag | Value | Description |
|---|---|---|
| `ACE_OBJECT_TYPE_PRESENT` | 0x00000001 | `ObjectType` GUID is present. |
| `ACE_INHERITED_OBJECT_TYPE_PRESENT` | 0x00000002 | `InheritedObjectType` GUID is present. |

Parsing rules:

- `AceSize` MUST be large enough to contain the header, mask, flags, all GUIDs selected by `Flags`, and a complete SID.
- Unknown bits in `Flags` MUST be ignored.
- If neither GUID-presence bit is set, the ACE has no GUID fields and behaves like the corresponding basic ACE.
- GUID fields are opaque 16-byte values at this layer. Their interpretation is described in the Peios Kernel TRM §3.8.5.

### 5.4.2.3 Callback ACE family

The following ACE types extend the corresponding non-callback ACE layout by
appending `ApplicationData` at the end of the ACE:

- `ACCESS_ALLOWED_CALLBACK_ACE`
- `ACCESS_DENIED_CALLBACK_ACE`
- `SYSTEM_AUDIT_CALLBACK_ACE`
- `SYSTEM_ALARM_CALLBACK_ACE`
- `ACCESS_ALLOWED_CALLBACK_OBJECT_ACE`
- `ACCESS_DENIED_CALLBACK_OBJECT_ACE`
- `SYSTEM_AUDIT_CALLBACK_OBJECT_ACE`
- `SYSTEM_ALARM_CALLBACK_OBJECT_ACE`

For non-object callback ACEs, the body layout is:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | AceHeader | Standard ACE header. |
| 4 | 4 | Mask | Access mask. |
| 8 | variable | Sid | Principal SID. |
| variable | variable | ApplicationData | Trailing type-specific bytes. Consumes the remainder of the ACE. |

For callback object ACEs, the body layout is:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | AceHeader | Standard ACE header. |
| 4 | 4 | Mask | Access mask. |
| 8 | 4 | Flags | Object ACE flags. |
| 12 | 0 or 16 | ObjectType | Present when `ACE_OBJECT_TYPE_PRESENT` is set. |
| 12 or 28 | 0 or 16 | InheritedObjectType | Present when `ACE_INHERITED_OBJECT_TYPE_PRESENT` is set. |
| variable | variable | Sid | Principal SID. |
| variable | variable | ApplicationData | Trailing type-specific bytes. Consumes the remainder of the ACE. |

Parsing rules:

- The SID begins after the fixed fields and any optional GUIDs, exactly as in the corresponding non-callback ACE family.
- `ApplicationData` MAY be empty. Semantics for empty or malformed callback payloads are defined by the relevant subsystem.
- For conditional ACEs, `ApplicationData` carries the conditional expression bytecode defined in the Conditional ACE Bytecode Reference.

### 5.4.2.4 Resource attribute ACE

`SYSTEM_RESOURCE_ATTRIBUTE_ACE` uses the single-SID ACE prefix followed by
trailing application data:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | AceHeader | Standard ACE header. |
| 4 | 4 | Mask | Reserved for compatibility. Not used for access decisions. |
| 8 | variable | Sid | MUST be Everyone (`S-1-1-0`). |
| variable | variable | ApplicationData | One claim entry using §5.9. Consumes the remainder of the ACE. |

Parsing rules:

- The SID MUST be Everyone.
- `ApplicationData` MUST contain exactly one claim entry using §5.9.

## 5.4.3 DACL ACE types

### 5.4.3.1 Basic ACEs

| Structure | Value | Effect |
|---|---|---|
| ACCESS_ALLOWED_ACE | 0x00 | Grants the specified rights to the SID. |
| ACCESS_DENIED_ACE | 0x01 | Denies the specified rights to the SID. |

### 5.4.3.2 Object-type ACEs

Extend basic ACEs with one or two GUIDs that scope the rule to a specific property or object class. Used for Active Directory access control.

| Structure | Value | Effect |
|---|---|---|
| ACCESS_ALLOWED_OBJECT_ACE | 0x05 | Grants rights scoped to a property/class GUID. |
| ACCESS_DENIED_OBJECT_ACE | 0x06 | Denies rights scoped to a property/class GUID. |

The ObjectType GUID identifies the property or property set the ACE applies to. The InheritedObjectType GUID restricts inheritance to child objects of a specific class. Either or both GUIDs MAY be absent (indicated by a flags field), in which case the ACE behaves like a basic ACE for that dimension.

### 5.4.3.3 Conditional ACEs

Extend basic and object-type ACEs with a conditional expression. The ACE only takes effect if the expression evaluates to TRUE against the caller's token attributes and the object's resource attributes.

| Structure | Value | Effect |
|---|---|---|
| ACCESS_ALLOWED_CALLBACK_ACE | 0x09 | Conditional allow. |
| ACCESS_DENIED_CALLBACK_ACE | 0x0A | Conditional deny. |
| ACCESS_ALLOWED_CALLBACK_OBJECT_ACE | 0x0B | Conditional allow, scoped to GUID. |
| ACCESS_DENIED_CALLBACK_OBJECT_ACE | 0x0C | Conditional deny, scoped to GUID. |

> [!NOTE]
> The term "callback" is historical. KACS evaluates conditional expressions inline during AccessCheck. The name is preserved for binary format compatibility. UI and UX layers MAY refer to callback ACEs exclusively as "conditional ACEs" for simplicity.

## 5.4.4 SACL ACE types

### 5.4.4.1 Audit ACEs

Trigger audit log entries when matching access attempts occur. The AceFlags field carries SUCCESSFUL_ACCESS_ACE_FLAG (0x40) and/or FAILED_ACCESS_ACE_FLAG (0x80).

| Structure | Value | Effect |
|---|---|---|
| SYSTEM_AUDIT_ACE | 0x02 | Audit access matching the SID and mask. |
| SYSTEM_AUDIT_OBJECT_ACE | 0x07 | Audit access scoped to a GUID. |
| SYSTEM_AUDIT_CALLBACK_ACE | 0x0D | Conditional audit. |
| SYSTEM_AUDIT_CALLBACK_OBJECT_ACE | 0x0F | Conditional audit, scoped to GUID. |

### 5.4.4.2 Alarm ACEs (continuous auditing)

> [!NOTE]
> These ACE types are reserved but unimplemented in the reference model. KACS repurposes them for continuous per-operation auditing: unlike standard audit ACEs (which emit a single event at handle creation), alarm ACEs configure per-operation audit masks that persist on the open handle. This creates no interoperability conflict because external sources never contain alarm ACEs.

| Structure | Value | Effect |
|---|---|---|
| SYSTEM_ALARM_ACE | 0x03 | Continuous audit for matching SID and mask. |
| SYSTEM_ALARM_OBJECT_ACE | 0x08 | Continuous audit scoped to a GUID. |
| SYSTEM_ALARM_CALLBACK_ACE | 0x0E | Conditional continuous audit. |
| SYSTEM_ALARM_CALLBACK_OBJECT_ACE | 0x10 | Conditional continuous audit, scoped to GUID. |

### 5.4.4.3 Mandatory label ACE

Defines the object's integrity level for MIC. Conforming producers SHOULD emit
at most one non-inherit-only mandatory-label ACE per SACL. Imported or existing
SACLs MAY contain multiple mandatory-label ACEs; MIC uses the first
non-inherit-only mandatory-label ACE as described in the Peios Kernel TRM
§3.8.3. Inherit-only
mandatory-label ACEs do not apply to the current object. The SID encodes the
integrity level. The access mask encodes the MIC policy (which operations are
blocked for non-dominant callers).

| Structure | Value | Effect |
|---|---|---|
| SYSTEM_MANDATORY_LABEL_ACE | 0x11 | Sets the object's integrity level and MIC policy. |

### 5.4.4.4 Resource attribute ACE

Attaches name-value attributes to the object for conditional ACE evaluation. The ACE's SID is always Everyone (`S-1-1-0`).

| Structure | Value | Effect |
|---|---|---|
| SYSTEM_RESOURCE_ATTRIBUTE_ACE | 0x12 | Defines a resource attribute on the object. |

### 5.4.4.5 Scoped policy ID ACE

References a central access policy by SID. During AccessCheck, the referenced policy's rules are evaluated in addition to the object's own DACL.

| Structure | Value | Effect |
|---|---|---|
| SYSTEM_SCOPED_POLICY_ID_ACE | 0x13 | References a central access policy. |

### 5.4.4.6 Process trust label ACE

Defines the object's PIP trust level. The SID encodes the PIP type and trust level. The access mask specifies the exact rights that non-dominant callers are allowed.

| Structure | Value | Effect |
|---|---|---|
| SYSTEM_PROCESS_TRUST_LABEL_ACE | 0x14 | Sets the object's PIP trust level. |

## 5.4.5 Allocated but unimplemented ACE types

Two values in the range have a name but no KACS behaviour.

| Structure | Value | Notes |
|---|---|---|
| ACCESS_ALLOWED_COMPOUND_ACE | 0x04 | Never implemented. Reserved. |
| SYSTEM_ACCESS_FILTER_ACE | 0x15 | Defined by MS-DTYP. Named by the KACS ABI for format parity; no KACS semantics. |

`ACCESS_ALLOWED_COMPOUND_ACE` was specified and then abandoned before any
conforming system implemented it. Nothing produces it.

`SYSTEM_ACCESS_FILTER_ACE` is different: it is a live MS-DTYP ACE type
that KACS has not implemented. The kernel ABI defines a constant for it
(`KACS_ACE_TYPE_SYSTEM_ACCESS_FILTER`) so that a decoder can put a name
to the byte, but no section of this specification assigns it meaning. A 0x15 ACE therefore takes the
unrecognised-ACE path described at the end of this section: skipped
during evaluation, preserved byte-for-byte on round-trip. An
implementation MUST NOT grant, deny, audit, or filter access on the
basis of a 0x15 ACE.

Values above 0x15 are unallocated.

## 5.4.6 AceType constants

The tables above name each ACE **structure**. The `AceType` header field
carries a constant with its own name, which is what a reader decoding a
descriptor by hand will be holding. Both spellings, in value order:

| Value | AceType constant | Structure |
|---|---|---|
| 0x00 | `ACCESS_ALLOWED_ACE_TYPE` | `ACCESS_ALLOWED_ACE` |
| 0x01 | `ACCESS_DENIED_ACE_TYPE` | `ACCESS_DENIED_ACE` |
| 0x02 | `SYSTEM_AUDIT_ACE_TYPE` | `SYSTEM_AUDIT_ACE` |
| 0x03 | `SYSTEM_ALARM_ACE_TYPE` | `SYSTEM_ALARM_ACE` |
| 0x04 | `ACCESS_ALLOWED_COMPOUND_ACE_TYPE` | `ACCESS_ALLOWED_COMPOUND_ACE` |
| 0x05 | `ACCESS_ALLOWED_OBJECT_ACE_TYPE` | `ACCESS_ALLOWED_OBJECT_ACE` |
| 0x06 | `ACCESS_DENIED_OBJECT_ACE_TYPE` | `ACCESS_DENIED_OBJECT_ACE` |
| 0x07 | `SYSTEM_AUDIT_OBJECT_ACE_TYPE` | `SYSTEM_AUDIT_OBJECT_ACE` |
| 0x08 | `SYSTEM_ALARM_OBJECT_ACE_TYPE` | `SYSTEM_ALARM_OBJECT_ACE` |
| 0x09 | `ACCESS_ALLOWED_CALLBACK_ACE_TYPE` | `ACCESS_ALLOWED_CALLBACK_ACE` |
| 0x0A | `ACCESS_DENIED_CALLBACK_ACE_TYPE` | `ACCESS_DENIED_CALLBACK_ACE` |
| 0x0B | `ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE` | `ACCESS_ALLOWED_CALLBACK_OBJECT_ACE` |
| 0x0C | `ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE` | `ACCESS_DENIED_CALLBACK_OBJECT_ACE` |
| 0x0D | `SYSTEM_AUDIT_CALLBACK_ACE_TYPE` | `SYSTEM_AUDIT_CALLBACK_ACE` |
| 0x0E | `SYSTEM_ALARM_CALLBACK_ACE_TYPE` | `SYSTEM_ALARM_CALLBACK_ACE` |
| 0x0F | `SYSTEM_AUDIT_CALLBACK_OBJECT_ACE_TYPE` | `SYSTEM_AUDIT_CALLBACK_OBJECT_ACE` |
| 0x10 | `SYSTEM_ALARM_CALLBACK_OBJECT_ACE_TYPE` | `SYSTEM_ALARM_CALLBACK_OBJECT_ACE` |
| 0x11 | `SYSTEM_MANDATORY_LABEL_ACE_TYPE` | `SYSTEM_MANDATORY_LABEL_ACE` |
| 0x12 | `SYSTEM_RESOURCE_ATTRIBUTE_ACE_TYPE` | `SYSTEM_RESOURCE_ATTRIBUTE_ACE` |
| 0x13 | `SYSTEM_SCOPED_POLICY_ID_ACE_TYPE` | `SYSTEM_SCOPED_POLICY_ID_ACE` |
| 0x14 | `SYSTEM_PROCESS_TRUST_LABEL_ACE_TYPE` | `SYSTEM_PROCESS_TRUST_LABEL_ACE` |
| 0x15 | `SYSTEM_ACCESS_FILTER_ACE_TYPE` | `SYSTEM_ACCESS_FILTER_ACE` |

## 5.4.7 ACL revision

ACLs carry a revision number that constrains which ACE types MAY appear:

- **ACL_REVISION (0x02)** — basic ACE types (0x00, 0x01, 0x02, 0x03), mandatory label (0x11), resource attribute (0x12), scoped policy (0x13), and process trust label (0x14).
- **ACL_REVISION_DS (0x04)** — additionally permits object-type ACEs (0x05–0x08), callback ACEs (0x09–0x0C, 0x0D–0x10). Required for Active Directory access control.

When creating new ACLs containing only recognised ACE types, the revision MUST
be set to the minimum required by the ACE types present. When rewriting an
existing ACL while preserving one or more unrecognised ACE types, KACS MUST set
the revision to the greater of the minimum required by recognised ACE types
present and the source ACL revision. When parsing ACLs, KACS MUST NOT reject an
ACL based on revision-vs-ACE-type mismatch — accept permissively, write
correctly.

> [!NOTE]
> This diverges from strict MS-DTYP interpretation, where ACL_REVISION (0x02) does not permit SACL types 0x11-0x14. KACS accepts them under either revision to handle SDs that may have been constructed with a less strict tool. The evaluator handles all ACE types correctly regardless of ACL revision.

Unrecognised ACE types — every value not given semantics above, which
today means 0x04, 0x15, and everything from 0x16 up — MUST be silently
skipped during evaluation and preserved byte-for-byte during round-trip serialisation. The ACE's raw bytes (from `AceType` through `AceType + AceSize`) are stored opaquely and written back unchanged. ACEs with `AceSize` not a multiple of 4 MUST be rejected (the containing ACL is malformed).

---

# 5.5 ACE Ordering

_Peios / Advanced Peios / PCDS / Security Descriptor_

> Canonical DACL order and why it is load-bearing — first-writer-wins means position decides the outcome — plus the ordering rules for a SACL.

The order of ACEs in a DACL determines the outcome of AccessCheck. AccessCheck walks the DACL from first ACE to last, and the first-writer-wins principle means each bit is decided at most once. ACE ordering is semantically load-bearing.

## 5.5.1 Canonical ordering

Tools that author SDs SHOULD produce canonically-ordered DACLs. KACS MUST NOT reject non-canonical DACLs — it evaluates whatever order it receives — but non-canonical ordering can produce results that contradict administrative intent.

The canonical order is:

1. **Explicit deny ACEs** — deny rules placed directly on this object (not inherited).
2. **Explicit allow ACEs** — allow rules placed directly on this object.
3. **Inherited deny ACEs** — deny rules inherited from parent objects (nearest parent first).
4. **Inherited allow ACEs** — allow rules inherited from parent objects.

Within each category, object-type ACEs (those scoped to a specific property GUID) SHOULD be ordered after whole-object ACEs.

This ordering guarantees: explicit rules override inherited rules, denials override allows at the same level, and whole-object rules override property-scoped rules.

KACS kernel paths that evaluate, store, query, or set caller-supplied DACLs MUST preserve the caller-supplied ACE order. KACS MUST NOT canonicalise those DACLs by reordering deny ACEs, allow ACEs, whole-object ACEs, or object-type ACEs.

When KACS constructs a file DACL by combining an explicit source DACL with an inherited source DACL, it MUST emit the explicit ACE sequence before the inherited ACE sequence. Within each sequence, KACS MUST preserve the source-relative ACE order. This recombination rule is not a general canonical sorting step.

## 5.5.2 SACL ordering

SACLs do not have a canonical ordering requirement. Audit ACEs are evaluated independently (each matching ACE generates its own audit event). The mandatory label ACE, resource attribute ACEs, and scoped policy ID ACEs are located by type scan, not by position.

---

# 5.6 SD Inheritance

_Peios / Advanced Peios / PCDS / Security Descriptor_

> How inheritable ACEs propagate from a container to a new child — the inheritance flags, CREATOR OWNER and CREATOR GROUP substitution, and the algorithm itself.

Security Descriptors propagate structurally. When a file is created in a
directory, the directory's inheritable ACEs flow down to the new file's
SD. This automatic propagation is inheritance.

Inheritance applies to objects with a container/child relationship:
directories contain files and subdirectories, registry keys contain
subkeys and values. Objects without a container parent (standalone IPC
endpoints, tokens, processes) do not inherit.

## 5.6.1 Inheritance flags

Four flags in the ACE header's AceFlags field control propagation:

| Flag | Value | Description |
|---|---|---|
| OBJECT_INHERIT_ACE (OI) | 0x01 | Inherited by non-container children (files). For container children (subdirectories), inherited as inherit-only unless NP is also set. |
| CONTAINER_INHERIT_ACE (CI) | 0x02 | Inherited by container children (subdirectories). The inherited ACE remains inheritable (propagates to grandchildren) unless NP is also set. |
| NO_PROPAGATE_INHERIT_ACE (NP) | 0x04 | When inherited, OI and CI flags are cleared on the copy. One-level inheritance. |
| INHERIT_ONLY_ACE (IO) | 0x08 | Does not apply to the object it is attached to. Exists only to be inherited by children. |

A fifth flag records provenance:

| Flag | Value | Description |
|---|---|---|
| INHERITED_ACE | 0x10 | Set on ACEs created through inheritance (not explicitly placed). Determines ordering in canonical form. |

## 5.6.2 Common flag combinations

| Flags | Meaning |
|---|---|
| CI \| OI | Inherit to everything — containers and non-containers, recursively. |
| CI | Inherit to containers only, recursively. |
| OI | Inherit to non-containers only. Containers receive it as inherit-only. |
| CI \| OI \| IO | Inherit to everything, but do not apply to this object. |
| CI \| OI \| NP | Inherit to immediate children only. |
| CI \| NP | Inherit to immediate child containers only. |
| (none) | No inheritance. Applies only to this object. |

## 5.6.3 CREATOR OWNER and CREATOR GROUP

Two well-known SIDs receive special treatment during inheritance:

- **CREATOR OWNER (`S-1-3-0`)** — when an ACE with this SID is inherited
  by a child object, the SID is replaced with the owner SID of the new
  object (as determined by the owner computation above).

- **CREATOR GROUP (`S-1-3-1`)** — replaced with the primary group SID of
  the creating principal.

Substitution happens at inheritance time. The resulting ACE on the child
contains the resolved SID, not the placeholder.

## 5.6.4 Inheritance algorithm

When a new object is created, its SD is computed from up to three
sources:

1. **Parent SD** — provides inheritable ACEs.
2. **Creator SD** — an explicit SD provided by the caller (if any).
3. **Creator token** — provides the default owner, primary group, and
   default DACL.

A creator SD with SE_SERVER_SECURITY set is rejected; see §5.1.

### 5.6.4.1 Owner

If the creator SD specifies an owner, use it. Otherwise, use the token's
owner SID.

### 5.6.4.2 Group

If the creator SD specifies a group, use it. Otherwise, use the token's
primary group SID.

### 5.6.4.3 DACL

The DACL is computed by merging explicit ACEs from the creator SD with
inheritable ACEs from the parent SD:

- If no creator SD is supplied and the parent has inheritable ACEs: the
  new object's DACL consists entirely of inherited ACEs from the parent.

- If no creator SD is supplied and the parent has no inheritable ACEs:
  the new object's DACL is the token's default DACL. If the token has no
  default DACL, the new object's DACL is null: SE_DACL_PRESENT is clear
  and the DACL offset is zero.

- If a creator SD is supplied but has no DACL (SE_DACL_PRESENT not set):
  the new object's DACL is computed as if no creator SD was supplied
  (inherit from parent, or fall back to the token's default DACL, or
  null DACL if the token has no default DACL).

- If a creator SD is supplied with a DACL (SE_DACL_PRESENT set):
  - Explicit ACEs from the creator SD are preserved.
  - If the creator SD's DACL is not protected (SE_DACL_PROTECTED not
    set) and SE_DACL_AUTO_INHERIT_REQ is set on the creator SD:
    inheritable ACEs from the parent are appended after the explicit
    ACEs. If SE_DACL_AUTO_INHERIT_REQ is not set, only the creator's
    explicit ACEs are used (no parent inheritance).
  - If the creator SD's DACL is protected: parent inheritance is
    blocked. Only the creator's explicit ACEs are used.

In all cases, the resulting DACL is post-processed:

- CREATOR OWNER / CREATOR GROUP SIDs are substituted with the actual
  owner and group. This substitution applies to the ACE's SID field
  only. ApplicationData — conditional expression bytecode — is copied
  verbatim: no SID substitution, no generic mapping, no offset
  adjustment. An implementation MUST NOT scan ApplicationData for
  CREATOR OWNER or CREATOR GROUP SIDs.
- Generic rights in all ACEs (both explicit and inherited) are mapped to
  object-specific rights via the object type's GenericMapping. This
  ensures no unresolved generic bits persist on stored ACEs. Generic
  rights appearing inside ApplicationData are not mapped.
- The INHERITED_ACE flag is set on all ACEs that came from the parent.
- If any ACE was inherited from the parent, SE_DACL_AUTO_INHERITED is
  set on the new SD's control flags; if the DACL came from the token's
  default DACL instead, SE_DACL_DEFAULTED is set. The equivalent applies
  to SE_SACL_AUTO_INHERITED for the SACL.

An ACE of an unrecognised type is carried to the child unchanged apart
from its AceFlags byte (§5.4). Its mask is not mapped and its SID is not
substituted, because neither can be located within an opaque ACE.

### 5.6.4.4 SACL

Computed identically to the DACL, substituting SACL for DACL throughout.
The token has no "default SACL" — if no creator SACL is supplied and the
parent has no inheritable SACL ACEs, the new object has no SACL.

## 5.6.5 Eager evaluation

Inheritance is eager. The new object's SD is fully computed at creation
time. There is no lazy inheritance — the kernel MUST NOT walk up the
directory tree at access time to find inheritable ACEs.

A consequence of eager evaluation: modifying an inheritable ACE on a
parent object does not automatically update existing children. Existing
children retain the SD they were created with. Propagating the change to
descendants is an explicit operation outside the scope of this document.
Children with SE_DACL_PROTECTED or SE_SACL_PROTECTED set MUST be skipped
during any re-propagation.

---

# 5.7 Ownership

_Peios / Advanced Peios / PCDS / Security Descriptor_

> The owner's implicit READ_CONTROL and WRITE_DAC, the OWNER RIGHTS ACE that suppresses them, and the rules for transferring ownership.

Every SD has an owner. Ownership confers two implicit rights that AccessCheck grants regardless of what the DACL says:

- **READ_CONTROL** — the owner can always read the object's SD.
- **WRITE_DAC** — the owner can always modify the object's DACL.

These implicit grants are the "you can't lock yourself out" guarantee. Even if the DACL grants the owner nothing, the owner can read and rewrite the DACL to restore access.

Ownership is determined by SID equality against the caller's user SID or token
group SIDs. Deny-only flags affect ACE matching, but they do not change the
ownership relation itself.

## 5.7.1 OWNER RIGHTS (S-1-3-4)

The implicit READ_CONTROL and WRITE_DAC grants MAY be suppressed or modified by including an ACE for the OWNER RIGHTS SID (`S-1-3-4`) in the DACL.

When AccessCheck detects any non-inherit-only access-control ACE targeting the OWNER RIGHTS SID in the DACL (via a pre-scan before the DACL walk), the implicit grant is suppressed. The owner's access then comes entirely from the DACL walk — through ACEs matching their user SID, group SIDs, and any ACEs targeting S-1-3-4.

This enables three patterns:

- **Suppress owner rights** — a deny ACE for S-1-3-4 with READ_CONTROL | WRITE_DAC.
- **Expand owner rights** — an allow ACE for S-1-3-4 with additional rights beyond the default.
- **Restrict owner rights** — an allow ACE for S-1-3-4 with only READ_CONTROL (no WRITE_DAC).

The OWNER RIGHTS pre-scan checks only for the presence of any non-inherit-only access-control ACE targeting the OWNER RIGHTS SID in the DACL, not whether any condition on the ACE evaluates to TRUE. A conditional ACE targeting OWNER RIGHTS suppresses the implicit grant even if the condition later evaluates to FALSE.

During the DACL walk, S-1-3-4 is treated as a normal SID. If the caller is the owner, ACEs targeting S-1-3-4 match the caller. The suppression only removes the automatic implicit grant; it does not isolate the owner from the rest of the DACL.

## 5.7.2 Ownership transfer

Changing an object's owner requires WRITE_OWNER on the object. Without SeTakeOwnershipPrivilege, the new owner MUST be the caller's own SID or a group on the caller's token with SE_GROUP_OWNER (flag value 0x00000008; see the Peios Kernel TRM §3.2.2).

SeTakeOwnershipPrivilege grants WRITE_OWNER on any object regardless of the DACL (deny-proof, but subject to MIC/PIP). SeRestorePrivilege bypasses the ownership SID constraint entirely — the `kacs_set_sd` syscall checks for SeRestorePrivilege and, when present, skips the "own SID or SE_GROUP_OWNER group" validation, allowing the caller to set ownership to any arbitrary SID.

---

# 5.8 Conditional ACEs

_Peios / Advanced Peios / PCDS / Security Descriptor_

> ACEs that carry a boolean expression — three-valued evaluation, attribute sources, SID matching, and the limits an expression must stay within.

Standard ACEs match on SID alone. Conditional ACEs add a boolean expression that MUST also evaluate to TRUE for the rule to take effect. This enables attribute-based access control (ABAC).

A conditional ACE is structurally identical to its non-conditional counterpart with a conditional expression appended after the SID. The expression is stored in a binary format defined by MS-DTYP §2.4.4.17.

## 5.8.1 Three-valued evaluation

Conditional expressions produce one of three results:

- **TRUE** — the condition is satisfied.
- **FALSE** — the condition is not satisfied.
- **UNKNOWN** — the condition could not be determined (missing attribute, type mismatch, malformed expression).

How the result affects the ACE depends on the ACE type:

| ACE type | TRUE | FALSE | UNKNOWN |
|---|---|---|---|
| Allow | ACE takes effect | ACE skipped | ACE skipped |
| Deny | ACE takes effect | ACE skipped | **ACE takes effect** |
| Audit | Event emitted | Event skipped | **Event emitted** |

This asymmetry is the fail-safe principle: uncertainty about whether to grant results in no grant; uncertainty about whether to deny results in denial.

## 5.8.2 Expression language

The expression supports:

- **Relational operators:** `==`, `!=`, `<`, `<=`, `>`, `>=`
- **Set operators:** `Contains`, `Any_of`, `Not_Contains`, `Not_Any_of`
- **Membership operators:** `Member_of`, `Member_of_Any`, `Not_Member_of`, `Not_Member_of_Any`, `Device_Member_of`, `Device_Member_of_Any`, `Not_Device_Member_of`, `Not_Device_Member_of_Any`
- **Logical operators:** AND, OR, NOT
- **Existence tests:** `Exists`, `Not_Exists`
- **Literal values:** integers, strings, SIDs, octet strings, composites

## 5.8.3 Three-valued logic

| AND | TRUE | FALSE | UNKNOWN |
|---|---|---|---|
| TRUE | TRUE | FALSE | UNKNOWN |
| FALSE | FALSE | FALSE | FALSE |
| UNKNOWN | UNKNOWN | FALSE | UNKNOWN |

| OR | TRUE | FALSE | UNKNOWN |
|---|---|---|---|
| TRUE | TRUE | TRUE | TRUE |
| FALSE | TRUE | FALSE | UNKNOWN |
| UNKNOWN | TRUE | UNKNOWN | UNKNOWN |

NOT: TRUE↔FALSE; UNKNOWN→UNKNOWN.

Boolean coercion for logical operands: integer nonzero → TRUE, zero → FALSE. String non-empty → TRUE, empty → FALSE. NULL → UNKNOWN. SID, octet, composite → UNKNOWN. Literal-origin values (values pushed directly from the bytecode, not obtained via attribute lookup) used as operands in AND/OR/NOT → UNKNOWN for the entire expression.

## 5.8.4 Attribute sources

Four attribute namespaces exist, resolved via bytecode opcodes:

| Opcode | Prefix | Source | Description |
|--------|--------|--------|-------------|
| 0xf9 | @User. | `token.user_claims` | Token-level claims set by authd at creation. |
| 0xfb | @Device. | `token.device_claims` | Device-level claims from the device token. |
| 0xfa | @Resource. | SD's SACL resource attribute ACEs | Per-object attributes, extracted in Pre-SACL walk. |
| 0xf8 | @Local. | `local_claims` parameter to AccessCheck | Per-call contextual attributes passed by the caller. Structured as a KACS claim array of length-prefixed claim entries, using §5.9. |

Attribute names are matched case-insensitively. `@User.Clearance`, `@User.clearance`, and `@User.CLEARANCE` all resolve the same attribute.

## 5.8.5 Claim flags

Claim flags apply to token claims (@User., @Device.) and resource attributes (@Resource.). @Local. claims also carry flags.

- **DISABLED (0x0010)** — the attribute is invisible to all conditions. Resolves as absent.
- **USE_FOR_DENY_ONLY (0x0004)** — the attribute participates only in deny-side conditional evaluation. Deny-side conditional evaluation includes deny ACE conditions and audit/alarm ACE conditions. For allow ACE conditions, it resolves as absent.

Empty attributes (zero values) are normalised to absent (NULL) at resolution time (when the expression evaluator reads the attribute value during AccessCheck).

Comparisons involving absent attributes evaluate to UNKNOWN. This includes
comparing two absent attributes with `==` or `!=`:
`@User.Missing == @Device.AlsoMissing` is UNKNOWN, not TRUE.

## 5.8.6 SID matching in expressions

The `Member_of` family evaluates group membership on the token. These operators are polarity-aware: deny-only groups do not satisfy allow-ACE conditions. Deny ACE conditions and audit/alarm ACE conditions use deny-side membership polarity, so enabled groups and deny-only groups both participate.

An empty SID operand set uses normal set semantics: `Member_of({})` and
`Device_Member_of({})` return TRUE, while `Member_of_Any({})` and
`Device_Member_of_Any({})` return FALSE. The `Not_*` forms are the logical
inverse of those results.

> [!NOTE]
> KACS makes virtual groups (S-1-3-4 for owner, S-1-5-10 for PRINCIPAL_SELF) visible to conditional expressions. `Member_of({S-1-3-4})` returns TRUE when the active SID-matching view treats the caller as the owner. Normal evaluation, restricted evaluation, and confinement evaluation may supply different virtual-group views as defined in their respective sections. This is an intentional divergence for semantic consistency between the SID matcher and the expression evaluator.

> [!NOTE]
> KACS promotes between INT64 and UINT64 for relational operators: negative INT64 is always less than any UINT64. Without promotion, UINT64 claims are unusable in conditions because the bytecode encodes integer literals as signed.

## 5.8.7 Binary format

Conditional expressions are encoded as a stack-based bytecode program in reverse Polish notation. The binary format is defined by MS-DTYP §2.4.4.17.4 and MUST be byte-compatible.

The expression bytecode begins with a 4-byte magic: `0x61 0x72 0x74 0x78` ("artx"). If the magic is absent or the expression is shorter than 4 bytes, evaluation MUST return UNKNOWN.

Evaluation succeeds only if the final stack contains exactly one tri-state result. If evaluation ends with zero entries, more than one entry, or a raw non-boolean value still on the stack, the expression MUST evaluate to UNKNOWN.

The full operator bytecodes and literal encodings are specified in §5.11. KACS implementations MUST be byte-compatible with MS-DTYP §2.4.4.17.4.

## 5.8.8 Limits

Implementations SHOULD enforce a maximum evaluation stack depth (recommended: 1024) and SHOULD return UNKNOWN for expressions that exceed it. Any bounds violation during parsing (reading beyond the expression buffer, underflowing the stack, integer overflow) MUST return UNKNOWN.

---

# 5.9 Claim Attribute Format

_Peios / Advanced Peios / PCDS / Security Descriptor_

> The CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1 entry — supported types, value encodings, single- and multi-entry containers, and validation.

KACS v0.20 uses a Windows-compatible `CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1`
entry format for:

- resource attributes in `SYSTEM_RESOURCE_ATTRIBUTE_ACE`
- token `user_claims`
- token `device_claims`
- `local_claims` passed to AccessCheck

The claim entry format itself is shared across all four surfaces. When multiple
entries are carried in one buffer (token claims or `local_claims`), KACS wraps
the Windows-compatible entry format in a simple length-prefixed sequence so the
buffer can be parsed deterministically without external metadata.

## 5.9.1 Supported types

KACS v0.20 supports these claim value types:

| Type | Value | Notes |
|---|---|---|
| `INT64` | `0x0001` | Signed 64-bit integer. |
| `UINT64` | `0x0002` | Unsigned 64-bit integer. |
| `STRING` | `0x0003` | UTF-16LE string. |
| `SID` | `0x0005` | Binary SID. |
| `BOOLEAN` | `0x0006` | Stored as `u64`; normalised to true/false at resolution time. |
| `OCTET` | `0x0010` | Byte array. |

`FQBN` (`0x0004`) is reserved and not supported in KACS v0.20. Any unsupported
claim type makes the containing claim entry invalid.

## 5.9.2 Entry layout

All multibyte integers are little-endian. All offsets are relative to the start
of the claim entry.

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | `NameOffset` | Offset to the UTF-16LE null-terminated attribute name. |
| 4 | 2 | `ValueType` | One of the supported claim value types above. |
| 6 | 2 | `Reserved` | Reserved. Ignored by AccessCheck. Producers SHOULD set to 0. |
| 8 | 4 | `Flags` | Claim flags. |
| 12 | 4 | `ValueCount` | Number of values. May be 0. |
| 16 | `4 * ValueCount` | `ValueOffsets[]` | One relative offset per value. Interpretation depends on `ValueType`. |

Claim flags use the same meanings everywhere this format appears:

| Flag | Value | Meaning |
|---|---|---|
| `CLAIM_SECURITY_ATTRIBUTE_VALUE_CASE_SENSITIVE` | `0x0002` | String/octet comparisons using this attribute are case-sensitive. |
| `CLAIM_SECURITY_ATTRIBUTE_USE_FOR_DENY_ONLY` | `0x0004` | The attribute is visible only to deny-side conditional evaluation: deny ACE conditions and audit/alarm ACE conditions. |
| `CLAIM_SECURITY_ATTRIBUTE_DISABLED` | `0x0010` | The attribute is invisible to conditional evaluation. |
| `CLAIM_SECURITY_ATTRIBUTE_MANDATORY` | `0x0020` | The attribute MUST NOT be removed or modified by unprivileged callers. `kacs_set_sd` rejects attempts to remove or modify a MANDATORY attribute unless the caller has SeTcbPrivilege. |

Unknown flag bits are preserved but have no defined semantics.

## 5.9.3 Value encodings

### 5.9.3.1 INT64 / UINT64 / BOOLEAN

For `INT64`, `UINT64`, and `BOOLEAN`, each `ValueOffsets[i]` points directly to
an 8-byte scalar:

- `INT64`: signed 64-bit integer
- `UINT64`: unsigned 64-bit integer
- `BOOLEAN`: unsigned 64-bit integer, normalised at resolution time:
  - `0` = false
  - any non-zero value = true

### 5.9.3.2 STRING

For `STRING`, each `ValueOffsets[i]` points to a 4-byte `u32` named
`StringOffset`. `StringOffset` then points to the actual UTF-16LE
null-terminated string.

Strings are stored without a separate length field. The terminating UTF-16
null (`0x0000`) MUST appear within the containing claim entry.

### 5.9.3.3 SID

For `SID`, each `ValueOffsets[i]` points to a 4-byte `u32` named `SidOffset`.
`SidOffset` then points to a binary SID in the standard SID wire format.

### 5.9.3.4 OCTET

For `OCTET`, each `ValueOffsets[i]` points to a 4-byte `u32` named
`OctetOffset`. `OctetOffset` then points to:

| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | `Length` | Byte length of the octet string. |
| 4 | `Length` | `Data` | Raw bytes. |

## 5.9.4 Single-entry containers

`SYSTEM_RESOURCE_ATTRIBUTE_ACE.ApplicationData` contains exactly one claim
entry and consumes the remainder of the ACE.

## 5.9.5 Multi-entry containers

Token claim buffers (`user_claims`, `device_claims`) and `local_claims` use a
KACS claim-array wrapper:

```
repeat until buffer exhausted:
  [entry_len:u32le]
  [entry_bytes: entry_len bytes]
```

Rules:

- `entry_len` MUST be non-zero.
- `entry_len` MUST fit entirely within the containing buffer.
- `entry_bytes` is one complete claim entry using the layout above.
- The parser consumes entries sequentially until the containing buffer length is
  exhausted exactly.

## 5.9.6 Validation rules

- The fixed header and `ValueOffsets[]` array MUST fit within the entry.
- Every offset and nested offset MUST remain within the entry bounds.
- Every string name and string value MUST terminate within the entry.
- Every referenced SID MUST be structurally valid.
- A malformed claim entry invalidates the containing surface:
  - malformed resource attribute ACE payload -> malformed SD for AccessCheck
  - malformed token claim buffer -> invalid token spec
  - malformed `local_claims` buffer -> invalid AccessCheck input

`ValueCount = 0` is valid. Empty attributes normalise to absent at resolution
time, as defined in §5.8.

---

# 5.10 Resource Attributes

_Peios / Advanced Peios / PCDS / Security Descriptor_

> Name-value metadata carried in the SACL for conditional ACEs to read — what resource attributes are, how they are stored, and which are protected.

A Security Descriptor MAY carry metadata about the object it protects — descriptive properties rather than access rules. These are resource attributes: name-value pairs stored as SYSTEM_RESOURCE_ATTRIBUTE_ACEs in the SACL.

Resource attributes do not grant or deny access. They exist so that conditional ACEs in the DACL can reference properties of the object during evaluation. A conditional allow ACE might say "grant read access if `@User.clearance >= @Resource.confidentiality`."

Each resource attribute ACE encodes a single named, typed, multi-valued
attribute. The name is a string. Values MAY be integers, strings, booleans,
SIDs, or byte arrays. The attribute data uses the claim entry format defined in
§5.9.

Multiple resource attribute ACEs MAY appear in the same SACL, each carrying a different attribute. If two ACEs carry the same attribute name, the first one wins — duplicates are silently ignored. Name comparison is case-insensitive, matching the conditional expression evaluator's attribute name matching.

An inherit-only `SYSTEM_RESOURCE_ATTRIBUTE_ACE` does not apply to the object it
is attached to and MUST be ignored during resource-attribute extraction.

Resource attributes are extracted from the SACL before the DACL walk begins, so they are available when conditional expressions need them.

A resource attribute marked `CLAIM_SECURITY_ATTRIBUTE_MANDATORY` is protected
metadata. Set-security operations MUST preserve each mandatory resource
attribute unless the caller has SeTcbPrivilege, as described in the Peios
Kernel TRM §3.4.2.

## 5.10.1 Claim types

| Type | Value | Description |
|---|---|---|
| INT64 | 0x0001 | Signed 64-bit integer. |
| UINT64 | 0x0002 | Unsigned 64-bit integer. |
| STRING | 0x0003 | Unicode string. |
| FQBN | 0x0004 | Fully Qualified Binary Name. Reserved — not supported in KACS v0.20. |
| SID | 0x0005 | Security identifier. |
| BOOLEAN | 0x0006 | Boolean value. |
| OCTET | 0x0010 | Byte array. |

Boolean values MUST be normalised to 1 (true) or 0 (false) at resolution time (when the conditional expression evaluator reads the attribute value), regardless of the wire encoding.

---

# 5.11 Conditional ACE Bytecode Reference

_Peios / Advanced Peios / PCDS / Security Descriptor_

> The postfix bytecode a conditional expression compiles to — magic signature, literal and operator tokens, and the integer encoding.

This section specifies the binary encoding for conditional ACE expressions. The format is byte-compatible with MS-DTYP §2.4.4.17.4. Conditional expressions are stored in the ApplicationData member of CALLBACK ACE types, encoded in postfix (reverse Polish) notation.

## 5.11.1 Magic signature

A CALLBACK ACE contains a conditional expression if the ApplicationData begins with `0x61 0x72 0x74 0x78` (the string "artx"). If the signature is absent or the expression is shorter than 4 bytes, evaluation MUST return UNKNOWN.

## 5.11.2 Token formats

Each token begins with a single byte-code identifying the token type. All multibyte integers, including Unicode characters, are stored least-significant byte first (little-endian). Expressions end at the ACE boundary; any bytes needed for DWORD alignment MUST be set to 0x00.

## 5.11.3 Literal tokens

| Token type | Byte-code | Token data encoding |
|---|---|---|
| Padding | 0x00 | No data. Used for DWORD alignment padding at end of expression. |
| Signed int8 | 0x01 | 1 QWORD (8 bytes LE) for the value (2's complement, range -128 to +127). 1 byte for sign. 1 byte for base. Total: 10 bytes. |
| Signed int16 | 0x02 | 1 QWORD (8 bytes LE) for the value (2's complement, range -32768 to +32767). 1 byte for sign. 1 byte for base. Total: 10 bytes. |
| Signed int32 | 0x03 | 1 QWORD (8 bytes LE) for the value (2's complement). 1 byte for sign. 1 byte for base. Total: 10 bytes. |
| Signed int64 | 0x04 | 1 QWORD (8 bytes LE) for the value (2's complement). 1 byte for sign. 1 byte for base. Total: 10 bytes. |
| Unicode string | 0x10 | 1 DWORD (4 bytes LE) for length in bytes. Then UTF-16LE code units (2 bytes each, LSB first). Not null-terminated. |
| Octet string | 0x18 | 1 DWORD (4 bytes LE) for length in bytes. Then raw bytes. |
| Composite | 0x50 | 1 DWORD (4 bytes LE) for total length in bytes of all contained elements. Then elements stored contiguously, each encoded per its own type rules. May be heterogeneous. |
| SID | 0x51 | 1 DWORD (4 bytes LE) for length in bytes. Then SID in binary representation (revision, sub-authority count, identifier authority, sub-authorities). |

### 5.11.3.1 Sign codes

Integer literals include a sign byte after the QWORD value:

| Sign | Code | Description |
|---|---|---|
| + | 0x01 | Explicit positive sign. |
| - | 0x02 | Negative. |
| None | 0x03 | No sign. Relational operators treat as positive. |

During relational evaluation, the sign byte determines the literal sign. Positive (`0x01`) and no-sign (`0x03`) literals MUST be evaluated as the positive magnitude of the QWORD value. Negative (`0x02`) literals MUST be evaluated as the negative magnitude of the QWORD value. If the resulting signed value does not fit the declared signed-width token, evaluation MUST return UNKNOWN.

### 5.11.3.2 Base codes

Integer literals include a base byte after the sign byte. The base is for display purposes only — the value is always stored as binary 2's complement regardless of base:

| Base | Code | Description |
|---|---|---|
| Octal | 0x01 | Display as octal. |
| Decimal | 0x02 | Display as decimal. |
| Hexadecimal | 0x03 | Display as hexadecimal. |

### 5.11.3.3 Integer encoding example

The decimal value -1 encoded as a signed int64:

```
0x04 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x02 0x02
 ^    ^-------- QWORD (2's complement) --------^  ^    ^
 |                                                 |    base=decimal
 byte-code=int64                                   sign=negative
```

## 5.11.4 Relational operator tokens

### 5.11.4.1 Binary relational operators

LHS is the second element on the stack, RHS is the top. If LHS and RHS are different types, the entire conditional expression evaluates to UNKNOWN — with the exception of INT64 and UINT64 which are promoted for comparison (see §5.8 for promotion rules). If either operand is UNKNOWN, the operation returns UNKNOWN.

| Token type | Byte-code | Processing |
|---|---|---|
| == | 0x80 | TRUE if RHS equals LHS (single or set value); FALSE otherwise. |
| != | 0x81 | FALSE if RHS equals LHS; TRUE otherwise. |
| < | 0x82 | TRUE if LHS < RHS; FALSE otherwise. |
| <= | 0x83 | TRUE if LHS <= RHS; FALSE otherwise. |
| > | 0x84 | TRUE if LHS > RHS; FALSE otherwise. |
| >= | 0x85 | TRUE if LHS >= RHS; FALSE otherwise. |
| Contains | 0x86 | TRUE if LHS value(s) include all of RHS value(s); FALSE otherwise. |
| Any_of | 0x88 | TRUE if RHS includes any of LHS value(s); FALSE otherwise. |
| Not_Contains | 0x8e | Logical inverse of Contains. |
| Not_Any_of | 0x8f | Logical inverse of Any_of. |

String and octet string comparisons are byte-by-byte, case-insensitive by default. If the CLAIM_SECURITY_ATTRIBUTE_VALUE_CASE_SENSITIVE flag (0x0002) is set on either operand's attribute, comparison is case-sensitive.

### 5.11.4.2 Unary relational operators (SID membership)

The operand is the top of the stack and MUST be a SID literal or a composite of SID literals.

| Token type | Byte-code | Processing |
|---|---|---|
| Member_of | 0x89 | TRUE if the token's group SIDs contain all SIDs in the operand. |
| Device_Member_of | 0x8a | TRUE if the token's device group SIDs contain all SIDs in the operand. |
| Member_of_Any | 0x8b | TRUE if the token's group SIDs contain any SID in the operand. |
| Device_Member_of_Any | 0x8c | TRUE if the token's device group SIDs contain any SID in the operand. |
| Not_Member_of | 0x90 | Logical inverse of Member_of. |
| Not_Device_Member_of | 0x91 | Logical inverse of Device_Member_of. |
| Not_Member_of_Any | 0x92 | Logical inverse of Member_of_Any. |
| Not_Device_Member_of_Any | 0x93 | Logical inverse of Device_Member_of_Any. |

For an empty SID operand set:

- `Member_of({})` and `Device_Member_of({})` return TRUE (vacuous truth).
- `Member_of_Any({})` and `Device_Member_of_Any({})` return FALSE.
- The `Not_*` forms are the logical inverse of those results.

## 5.11.5 Logical operator tokens

Logical operators test the logical value of operands and produce TRUE, FALSE, or UNKNOWN. The logical value of an operand is determined by:

- Literal-origin value → error (entire expression returns UNKNOWN)
- Attribute with null value → UNKNOWN
- Attribute with integer value → TRUE if nonzero, FALSE if zero
- Attribute with string value → TRUE if non-empty, FALSE if empty
- Result value → the result's tri-state value

### 5.11.5.1 Unary logical operators

| Token type | Byte-code | Processing |
|---|---|---|
| Exists | 0x87 | TRUE if the operand is an attribute (@Local., @Resource., @User., or @Device.) with a non-null value. FALSE if the attribute is absent or null. Returns error (→ UNKNOWN) for literal operands. KACS divergence: extends Exists to all four namespaces (MS-DTYP restricts to Local/Resource only). |
| Not_Exists | 0x8d | Logical inverse of Exists. |
| NOT (!) | 0xa2 | TRUE→FALSE, FALSE→TRUE, UNKNOWN→UNKNOWN. |

### 5.11.5.2 Binary logical operators

LHS is the second element on the stack, RHS is the top.

| Token type | Byte-code | Processing |
|---|---|---|
| AND (&&) | 0xa0 | If either operand is FALSE, return FALSE. Else if either is UNKNOWN, return UNKNOWN. Else return TRUE. |
| OR (\|\|) | 0xa1 | If either operand is TRUE, return TRUE. Else if either is UNKNOWN, return UNKNOWN. Else return FALSE. |

## 5.11.6 Attribute reference tokens

Attribute names are encoded as Unicode strings (same format as the 0x10 literal: DWORD length + UTF-16LE code units). The byte-code determines which namespace to look up the attribute in.

Attribute lookup is case-insensitive: the encoded name is matched against the namespace's attribute names without regard to case.

| Token type | Byte-code | Namespace |
|---|---|---|
| @Local. | 0xf8 | Local claims (passed as AccessCheck parameter). |
| @User. | 0xf9 | User claims (from token.user_claims). |
| @Resource. | 0xfa | Resource attributes (from SACL resource attribute ACEs). |
| @Device. | 0xfb | Device claims (from token.device_claims). |

## 5.11.7 Complete byte-code summary

For quick reference, all byte-codes in numeric order:

| Byte-code | Token |
|---|---|
| 0x00 | Padding |
| 0x01 | Signed int8 literal |
| 0x02 | Signed int16 literal |
| 0x03 | Signed int32 literal |
| 0x04 | Signed int64 literal |
| 0x10 | Unicode string literal |
| 0x18 | Octet string literal |
| 0x50 | Composite literal |
| 0x51 | SID literal |
| 0x80 | == |
| 0x81 | != |
| 0x82 | < |
| 0x83 | <= |
| 0x84 | > |
| 0x85 | >= |
| 0x86 | Contains |
| 0x87 | Exists |
| 0x88 | Any_of |
| 0x89 | Member_of |
| 0x8a | Device_Member_of |
| 0x8b | Member_of_Any |
| 0x8c | Device_Member_of_Any |
| 0x8d | Not_Exists |
| 0x8e | Not_Contains |
| 0x8f | Not_Any_of |
| 0x90 | Not_Member_of |
| 0x91 | Not_Device_Member_of |
| 0x92 | Not_Member_of_Any |
| 0x93 | Not_Device_Member_of_Any |
| 0xa0 | AND (&&) |
| 0xa1 | OR (\|\|) |
| 0xa2 | NOT (!) |
| 0xf8 | @Local. attribute |
| 0xf9 | @User. attribute |
| 0xfa | @Resource. attribute |
| 0xfb | @Device. attribute |

KACS implementations MUST be byte-compatible with these encodings.

> [!NOTE]
> This byte-code table is reproduced from MS-DTYP §2.4.4.17.4 through §2.4.4.17.8 for specification self-containment. KACS-specific evaluation semantics (three-valued logic tables, INT64/UINT64 promotion, virtual group visibility in Member_of, polarity-aware SID matching) are defined in §5.8.

---

# Appendix 5.A Prior Art

_Peios / Advanced Peios / PCDS / Security Descriptor_

> Where the SD family comes from in MS-DTYP — the self-relative descriptor, the ACL and ACE formats, claim entries, and conditional bytecode.

## 5.A.1 MS-DTYP

The security descriptor family defined in this chapter derives from
the Microsoft Data Types specification (MS-DTYP): the self-relative
security descriptor (§2.4.6), the ACL and ACE binary formats, the
CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1 claim entry, and the conditional
expression bytecode (§2.4.4.17). Peios keeps the binary formats
byte-compatible so security descriptors round-trip with Windows
systems — over the wire and on NTFS volumes.

Deliberate divergences from the reference model are flagged inline as
notes where they occur: permissive ACL-revision parsing, the
repurposing of alarm ACEs for continuous auditing, evaluation-time
generic mapping, the extension of Exists to all four attribute
namespaces, INT64/UINT64 promotion in relational operators, and
virtual-group visibility in Member_of.
