RFC 0026 — The ACE access_mask canonical wire width is u32¶
Note
Status: accepted. This page is an accepted change proposal, kept as the record of why the specification reads as it does. RFCs are proposals and history, not the standard. The normative specification is Protocol v1 and the annexes its §3 incorporates; where an RFC and the specification differ, the specification wins. All RFCs, with their status, are listed in the ADR and RFC index.
Field |
Value |
|---|---|
RFC |
0026 |
Title |
The ACE |
Status |
accepted (2026-08-12, maintainer-ratified in the #993 grilling session; doc + vector + binding edits land in the acceptance train) |
Author(s) |
AvatarSD (maintainer) |
Created |
2026-08-13 |
Comment window |
waived by default while solo-maintained (GOVERNANCE.md §”Errata, amendments, and the comment window”). Verified: |
Instrument |
Amendment. Reference 05 is normative by incorporation (spec v1 §3, ADR-0007), its § |
Tracking issue |
|
Target spec version |
v1 itself — the immutability clause has never triggered ( |
1. Summary¶
The ACE access_mask field (reference 05 §0x0A) had two
spellings in the published corpus: the layout text, the acl/acl-aces conformance vector and the
Rust Ace builder said u16, while the C++ typed builder (tr::graph::encode_acl) — the
encoder behind every :acl read a reference node serves — emitted u32. Nothing misparsed
(the acceptance rule bounds a numeric ACE payload at no wider than the parsed width, and
little-endian narrowing is exact), but the two cores emitted different bytes for the same typed
ACE and only one of them matched the published vector.
This RFC names u32 as the one canonical width and aligns every artifact to it:
Reference 05 §
0x0A: the layout line becomesVALUE <u32 bitfield>; the paragraph that disclosed the divergence and linked #993 is rewritten, because the divergence no longer exists.The
acl/acl-acesvector: re-cut with 4-byteaccess_maskchildren (179 → 183 bytes).The Rust binding:
structured::Ace::access_maskwidensu16→u32, its builder emits four bytes, and its reader stops narrowing the parsed mask to 16 bits.The C++ core:
encode_aclalready emits u32 — unchanged.
2. Why u32, not u16¶
Reference 05’s own bit registry ends with “
0x100+ reserved” — text that is dead the day the wire width is u16 with0x8000the last expressible bit after eight are assigned. The u16 spelling was never the intent; it was an under-specified transcription.Canonicalizing on u16 would put a permanent 16-bit ceiling on the rights registry of a security surface to save two bytes per ACE on a control-plane field — the wrong trade.
The C++ core’s
:aclread path has emitted u32 sinceencode_aclexisted, so u32 is also the width with deployment inertia.
3. What does NOT change¶
parse_acl’s acceptance rule (non-empty && size ≤ parsed width, #906) is untouched: a payload narrower than u32 still parses and zero-extends — the old two-byte spelling remains readable, so this amendment obsoletes no stored ACL bytes. Tightening acceptance to exactly-canonical was considered and rejected in the #993 ruling as #906’s scope.The in-memory C++ type (
ace_t::access_mask,std::uint32_t) and the bit assignments.The ACL evaluation model, inheritance, and the reserved-subject rule.
4. Gating¶
The conformance harness gates the codec only (encode(decode(input.bin)) == input.bin) and can
never catch a typed-builder width divergence, so each core gains a host test that consumes its
typed builder against the vector bytes: the C++ suite byte-compares encode_acl of the
vector’s ACE list with input.bin, and the Rust suite already byte-compares structured::acl
with the vector (plus a new round-trip pinning that a mask above the old 16-bit ceiling survives
build → decode → read untruncated).