Grants judge objects
A grant prefix carries two forms, surface as written and real with
symlinks followed, and every authority is judged on real. fs
authorises the objects the kernel touches; exec authorises the file a head
will execute (exec-rules),
which is the object the kernel judges too. The surface is what the author
wrote, kept for rendering and display, never a door to authority.
FrozenPath (core/src/path/forms.rs) freezes both at one disk
consultation and offers containment on the real form alone:
- fs —
FrozenPath::contains::<P>, to which aRegion’sScope::holdsdelegates. The access side is canonicalised (canon.rs) or walked symlink-free (walk.rs) before the question is asked, so a link planted inside a granted region is matched where it lands (object-not-name). - exec —
FrozenPath::real, the one door from a prefix to its real form: path and dir keys enterExecRulesas their real forms, judged against a head’srealpath(3)byRealPath::within::<P>, the rule’s polarity choosing the identity. - identity — native. Both forms are native paths: a name that is not Unicode is its own name under every identity, never the U+FFFD sibling it decodes to. An allowance whose real form is not Unicode is refused at freeze, since it is serialized and kept for good; an exclusion’s live real form is kept byte-exact, and holds.
- composition —
Table::meet, which joins exclusions and keeps an allowance where both layers support its own subject, judges the same real forms the in-process guard does. No sweep evicts an allowance: an exclusion beats it as evidence, never by deleting it.
An object can answer to several names — case and Unicode normalisation on
APFS, NTFS, ZFS and Linux casefold directories — and an absent one has no stored
spelling yet. So a deny is judged by collision class and an allow by stored
name (denies-hold-under-every-spelling):
a rule of a Table speaks through Scope::holds::<P>, its evidence bit choosing
the Polarity, and path maps Deny to Identity::Collision, keyed by
identity::collision_key, and Allow to Identity::Stored. A deny then holds the
create that would make its object under another spelling, where an allow never
reaches a distinct name on a case-sensitive volume. Composition keeps the
split: exclusions join, allowances meet by stored name, and the meet writes a
negative bit only where a layer wrote one, since a default so written would hold every other
spelling of its name (authority-tables).
A resolved prefix means what the disk said at freeze, and an allowance keeps
that meaning for good: a granted ~/bin that is a link to /opt/x/bin grants
/opt/x/bin, and a link planted at ~/bin later redirects nothing. Only an
exclusion re-resolves, so it holds under whatever its name reaches now
(authority-is-belnap-evidence). That is no re-aiming of the
author’s intent — a head spelled under ~/bin executes there, and so the
kernel has always judged it.
How it is held
The rule is which form, so the only way to get it wrong is to be handed a
choice. Nothing outside core/src/path/ is:
identity::path_withinandidentity::path_within_str, the form-blind kernel, arepub(super), so no caller elsewhere re-derives a matcher over whichever form it holds, as exarch’s skill check once did.- The identity is chosen by a rule’s polarity, a type parameter: a table
names
AlloworDeny, never anIdentity, so a deny has no exact question to ask. surface_pathis private toforms.rs; outsidepath, the surface leaves the type only as aString, for rendering.
A guard must ask on the form its check matches. The one time it did not, the
xdg-* freeze guard asked containment of the surface while the check it guarded
matched the real form, and read XDG_DATA_HOME=~/link, link → /etc, as
contained inside HOME (fixed in a5b0a525).
See also capability-carriers, two-enforcers, map: capabilities.