Appendix 3.D KACS ABI Notes
§3.A is generated from pkm/uapi/pkm/ and holds only what a compiler
can measure. This appendix holds the rest: the two ACE types that have
a constant and no behaviour, the payload shapes behind the token query
classes, the specification spellings a reader may arrive holding, what
is deliberately documented elsewhere, and the kernel configuration
KACS is built by.
The split is structural rather than editorial. gen-kacs-abi.py
overwrites §3.A wholesale on every run, so anything written there is
lost the next time the ABI changes — which is exactly what happened to
two sections of this one before they were moved here.
3.D.1 ACE types with no evaluator behaviour #
Two of the ACE type constants in §3.A have a constant and nothing
behind it. The ACE parser in kacs-core dispatches on 0x00–0x03,
0x05–0x14 and classifies every other value as opaque, so an ACE of type
0x04 or 0x15 is skipped during evaluation and written back
byte-for-byte on serialisation. The constants exist so that a decoder
can put a name to the byte. libpeios' SDDL codec does, printing 0x15 as
SYSTEM_ACCESS_FILTER; the sd utility does not, and renders both as
OTHER(0x04) and OTHER(0x15). PCDS §5.4 records the same state
normatively.
3.D.2 Querying a token handle #
The ioctl is straightforward in shape:
ioctl(token_fd, KACS_IOC_QUERY, &args)
Where args is a kacs_query_args struct:
| Field | Meaning |
|---|---|
token_class | The numeric class identifying what to return (1–24 in v0.20). |
buf_len | Input: the size of the output buffer in bytes. Output: the actual number of bytes the query needed. |
buf_ptr | Userspace pointer to the output buffer. |
The kernel:
- Validates the class against the catalog. Unknown classes return
-EINVAL. - Checks that the fd grants
TOKEN_QUERY. If not, returns-EACCES. - Computes the size the response needs.
- If
buf_ptris zero orbuf_lenis zero — this is a size query — writes the required size tobuf_lenand returns 0. - If
buf_ptris non-zero butbuf_lenis smaller than required, returns-ERANGEwith the required size still written tobuf_len. - Otherwise writes the response to the buffer and returns 0.
The "two-call pattern" — size query then fetch — is the standard way to handle variable-length output:
- Call once with
buf_ptr = NULL(orbuf_len = 0). The kernel writes the required size intobuf_lenand returns 0. - Allocate a buffer of the indicated size.
- Call again with
buf_ptrset to the buffer andbuf_lenset to its size. The kernel writes the response.
For classes with a fixed-size response, a single call with a buffer of the known size works in one go. The two-call pattern is needed only for classes whose response size depends on the token's contents (the groups class, the restricted-SIDs class, the default-DACL class, the claims classes).
The ioctl is idempotent — multiple queries for the same class produce the same result as long as the token has not been modified. Tokens carry a modified_id counter that increments on adjustment; if a query is part of a pipeline that depends on consistency across multiple queries, the modified_id can be queried first to detect mid-pipeline changes.
3.D.3 Token query payloads #
The class numbers come from the header and are tabulated in §3.A;
these are the payloads each one returns. Sizes are in bytes; a variable-length payload uses
the shapes below. An invalid class returns EINVAL.
Two repeating shapes appear throughout. A SID array is
[count:u32le] followed by count entries of
[sid_len:u32le][sid_bytes][attributes:u32le], and reports a count of
zero when the array is empty rather than an empty payload. A claims
array is [count:u32le] followed by count entries of
[entry_len:u32le][entry_bytes]. A bare SID is the SID bytes alone,
and an absent optional SID or ACL is zero bytes.
KACS_TOKEN_CLASS_GUID (0x19) uses the standard query contract: a size probe reports 16, a non-probe buffer smaller than 16 returns ERANGE without a partial payload, and every query requires TOKEN_QUERY. TOKEN_QUERY_SOURCE alone does not grant it.
Adjusting a token preserves its GUID; duplicating or restricting it creates a distinct token with a new GUID. Reopening the same token returns the same GUID. The STATISTICS payload remains 40 bytes and its token id is a LUID, not this GUID.
Querying the effective token returns the bytes stamped as KMES effective_token_guid; querying the real primary token returns true_token_guid. Impersonation changes which token the effective open selects, not either token's identity.
Several token fields have no query class at all: created_at,
audit_policy, write_restricted, user_deny_only,
isolation_boundary, confinement_exempt, the projected UID and GID
— only the supplementary GIDs are reportable —
restricted_device_groups, and the LCS registry credentials.
3.D.4 Names that differ from the specifications #
This manual uses the names uapi/pkm/ declares, and the generated
tables of §3.A are authoritative for them. A reader may instead arrive
holding the name PCDS uses, which is MS-DTYP's — a legitimate spelling,
not an obsolete one, and the one a third party implementing PCDS will
have. This table maps those onto the headers.
| PCDS / MS-DTYP | uapi name |
|---|---|
ACCESS_ALLOWED_ACE_TYPE, SYSTEM_AUDIT_ACE_TYPE, ... | KACS_ACE_TYPE_ACCESS_ALLOWED, KACS_ACE_TYPE_SYSTEM_AUDIT, ... (the qualifier moves to the front) |
KACS_REAL_TOKEN | KACS_TOKEN_OPEN_REAL |
KACS_LEVEL_* | KACS_IMLEVEL_* |
KACS_FILE_SUPERSEDE, _OPEN, ... | KACS_DISPOSITION_* |
OWNER_SECURITY_INFORMATION, ... | KACS_SECINFO_* |
SE_PRIVILEGE_ENABLED / _REMOVED | KACS_PRIVILEGE_ATTR_ENABLED / _REMOVED |
KACS_PRIV_RESET_ALL_DEFAULTS | KACS_PRIVILEGE_RESET_ALL_DEFAULTS |
KACS_RESTRICT_WRITE_RESTRICTED | KACS_TOKEN_RESTRICT_WRITE_RESTRICTED |
SE_GROUP_* | KACS_SID_GROUP_* |
TOKEN_CLASS_* | KACS_TOKEN_CLASS_* |
The PIP tiers have no public names at all. The Protected type (512)
and the PeiosTcb trust level (8192) exist only as kernel-private
constants, and nothing in uapi/pkm/ defines None, Protected or
Isolated. A program reasoning about tiers compares the numbers
(§3.7).
3.D.5 Socket options and retired syscall numbers #
Peer identity on AF_UNIX sockets is read and bounded through socket
options rather than syscalls. uapi/pkm/socket.h defines the option
level SOL_KACS (4096), its options KACS_SO_PEER_TOKEN,
KACS_SO_IMPERSONATION_LEVEL, KACS_SO_PASS_TOKEN and
KACS_SO_RESTAMP, and the ancillary message type KACS_SCM_TOKEN,
which travels at
cmsg_level SOL_KACS (§3.A). The kernel dispatches the option level
in net/socket.c ahead of the protocol's own handlers (patch
net/socket-sol-kacs-dispatch.patch), so the options reach KACS on
every socket family and KACS decides which it supports. The ancillary
message is carried per skb on AF_UNIX: net/scm-kacs-token.patch
gives struct scm_cookie a counted token reference and parses
SOL_KACS control messages; net/af_unix-kacs-token.patch carries
the reference on struct unix_skb_parms, stops a stream read at an
identity boundary, and advances the register at the point of
consumption. Everything KACS-specific behind those hooks lives in
kacs/socket.c. The semantics and error codes are in §3.5.3.
System V IPC objects carry descriptors of their own (§3.11); their
rights are uapi/pkm/ipc.h, and the same header defines the
KACS_SD_AT_SYSV_* flags that let kacs_get_sd and kacs_set_sd
address such an object by kind and id (dirfd carries the id, the
path is NULL). The lookup is ipc_lsm_with_object, exported from
ipc/util.c by ipc/util-lsm-with-object.patch; the hooks themselves
are the LSM's own IPC hooks and need no patch.
Three syscall numbers are retired and left as permanent holes:
1010 (kacs_open_peer_token), 1011 (kacs_impersonate_peer) and
1013 (kacs_set_impersonation_level). A binary built against them
gets ENOSYS. The first became getsockopt(SOL_KACS, KACS_SO_PEER_TOKEN), the third setsockopt(SOL_KACS, KACS_SO_IMPERSONATION_LEVEL), and the second was a fusion of the
first with KACS_IOC_IMPERSONATE that now lives in libpeios as
peios_token_impersonate_peer.
3.D.6 Open-interface documentation discrepancies #
The comparison below records historical documentation differences. The pinned source findings resolve specific creator-SD, create-option and no-follow questions; they do not reconcile every group below. No runtime behavior or historical release boundary was tested or established. The generated ABI supplies layouts and constants; it does not settle these behavioral questions.
Source labels and line numbers refer to the learn snapshot
4b119864d7f51c569ae1ac1932e4430f06731cf2:
- O: the former Security Fundamentals Opening files source, retained in that revision's history.
- N: KACS-Native Open, TRM §3.9.2.
- L: Legacy Open Compatibility, TRM §3.9.3.
- D: Opening a file, SDK reference.
- G: Securing files, SDK guide.
The links reach the current references; the line numbers identify the comparison snapshot above.
-
Maximum allowed. O:116–127 says
MAXIMUM_ALLOWEDreturns the maximum mask without checking the other requested bits, which it calls hints; N:14–19 says the concrete data/execute bits must be granted. Both require a concrete bit and rejectMAXIMUM_ALLOWEDalone. The general DACL walk describes a separate AccessCheck layer and cannot settle open validity by analogy. -
Legacy masks. O:135–145 maps
O_RDONLYtoFILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | READ_CONTROL | SYNCHRONIZE,O_WRONLYtoFILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | READ_CONTROL | SYNCHRONIZE, andO_RDWRto their union. O:154–158 makes onlyFILE_READ_DATAcore for read-only opens and allowsFILE_READ_ATTRIBUTESto be dropped. L:16–37 instead makesFILE_READ_ATTRIBUTEScore with read data, write data, or both according to the flag; L:39–49 lists broader compat rights. O:138 saysO_APPENDaddsFILE_APPEND_DATA; L:30–33 replaces coreFILE_WRITE_DATAwith it, then re-adds write data forO_TRUNC. L:44–45 also requests write data as optional compat access on append opens. These are different mappings, not interchangeable summaries. -
Creator descriptors. O:96–97 groups open-existing branches under
EINVALand listsSUPERSEDEonly for an absent target. N:133–144 distinguishesFILE_OPEN(EOPNOTSUPP) from existingFILE_OPEN_IF,FILE_OVERWRITEandFILE_OVERWRITE_IF(EINVAL); N:82–89 also permits a caller-supplied SD on replacement byFILE_SUPERSEDE. D:36–46 and G:19–36 showOPEN_IFwith a non-null creator SD and imply that the existing-file branch succeeds, despite O and N rejecting it. The SDK examples have now been changed to create-only. The pinned source findings below settle these existing-object rejections; this pass does not settle the separateSUPERSEDEcomparison. -
Replacement and status. O:71 says
SUPERSEDEremoves the inode and recommends it for atomic-replace patterns. N:82–89 replaces the pathname while preserving old hardlinks and already-open references. O:112 claims nonconditional dispositions predict the status; N:176–179 says an absent-targetFILE_SUPERSEDEreportsCREATED, withSUPERSEDEDonly for actual replacement. No atomicity guarantee follows from this comparison. -
Delete-on-close. O:80 describes deletion on the last fd referencing the file. N:112–129 instead specifies final close of one file-description lineage, preserved by
dup,forkandSCM_RIGHTS; later opens fail closed, and only regular files are supported. Generic inode last-reference semantics and this no-share lineage boundary differ. -
Unverified raw details. O:88 claims native
AT_EMPTY_PATHopens an empty path against the directory referenced bydirfd; N:66–69 discusses that flag for get/set-security, not native open. O:140–143 mapsO_CREATtoOPEN_IFwith parentFILE_ADD_FILE,O_CREAT | O_EXCLtoCREATE, andO_NOFOLLOWtoAT_SYMLINK_NOFOLLOW. Their exact legacy translation and parent-right timing are not established by N/L/D. O:170 attributes legacy creation to umask-based defaults plus parent inheritable ACEs; Inheritance describes parent, creator and token SD sources without establishing that umask contribution. O:183,185 also lists path-componentENOTDIRand invalid-dispositionEINVAL. These remain earlier, unverified claims; omission from another reference does not prove rejection or support. O:180 distinguishes a failed open access check from an unreachable path component; directory traversal is a separate authorization check. -
SDK destination caveats. D:3 mentions share mode; D:28 describes no-follow, write-through and the rest of the
NtCreateFileoption set. N:111–120 instead lists two supported create-option bits, reserves all others, and says the ABI has no share-mode field. D:26 calls the native result a “granted subset,” while N:6–19 distinguishes strict requests from maximum-allowed requests. The SDK reference is a programming entry point, not evidence that every difference has been reconciled. The current SDK page no longer advertises share mode or unsupported create options; the pinned source below verifies the accepted option bits and their SDK field mapping. The granted-subset wording versus strict/maximum semantics remains part of the unresolved comparison.
3.D.6.1 Pinned source findings #
A bounded source check used kernel
8e0e22de3a59cad506bbbf8873de456e16ad272d and libpeios 0.5.8
de0018bdcaed14abb796aeccec9c8387fd30e452. These are implementation findings
for those commits, not proof about a deployed kernel or the release where a
behavior began. Referenced KUnit tests are definitions; they were not executed.
- Creator SDs (part of group 3). Argument validation
rejects
OPENplus creator SD withEOPNOTSUPPandOVERWRITEplus creator SD withEINVAL. Existing-path handling rejectsOPEN_IFandOVERWRITE_IFplus creator SD withEINVALwhen the object exists. The OPEN_IF test records that expectation. SDK marshalling forwards the disposition and SD, rather than stripping the SD on an existing path. Use create-only with an explicit SD or open-existing withNULLSD; anEEXISTretry is a separate operation, not an atomic transaction. - Create options (part of group 7). The accepted-bit mask
contains only
DIRECTORYandDELETE_ON_CLOSE; unsupported bits returnEINVAL. The same SDK marshalling copiesoptionstocreate_optionsandflagstoflags, soAT_SYMLINK_NOFOLLOWbelongs in the latter. - No-follow is operation-specific. Native open
rejects a terminal symlink with
ELOOP. The by-path SD query instead selects the link itself through its lookup flags and path resolver, as recorded by a link-query test. The SDK query path forwards the flags; its buffer-size adaptation does not impose a terminal-link rejection. This query finding does not verify a successful link-SD write or remove the set-security component and storage checks.
Groups 1, 2, 4, 5 and 6 remain unresolved by this check, as do the other behavioral claims in groups 3 and 7. Do not treat the TRM, SDK or generated ABI as a blanket resolution of those differences.
3.D.7 What is not here #
Required rights, error codes and validation rules are properties of the implementation rather than of the headers, so they are documented with the operations themselves: token rights and the per-ioctl requirements in §3.2.8, the file rights in §3.9, the process rights in §3.3.3, and the privileges in §3.4.2.
Two neighbouring ABIs are generated or documented separately.
uapi/pkm/trace.h is a versioned, append-only ABI of tracepoint
reason, operation and state codes intended for tooling.
uapi/pkm/kmes.h and uapi/pkm/lcs.h belong to their own chapters.
3.D.8 Build configuration #
CONFIG_SECURITY_PKM=y and CONFIG_RUST=y are required, as are
CONFIG_STRICT_DEVMEM=y and CONFIG_MODULE_SIG_FORCE=y. The last two,
and the absence of every other MAC LSM and of CONFIG_BPF_LSM, are
enforced at build time by BUILD_BUG_ON in pkm_init (§3.7): a kernel
configured without them does not compile. They were once checked at
initialisation instead, which is no refusal at all -- the LSM framework
answers a failed init with a warning and boots with the hooks
uninstalled, the opposite of what the gate intends (PEI-487).
CONFIG_SECURITY_SELINUX, _APPARMOR, _SMACK and _TOMOYO are
additionally refused by Kconfig dependency. CONFIG_LSM is never
parsed.
Two further symbols gate large bodies of code: