Inheritance
When a new object is created under a parent — a file in a directory, a registry key under another key — the new object gets its own security descriptor. The kernel computes that SD at creation time by combining inheritable ACEs from the parent's SD with whatever the creator supplied or defaulted. The result is stored on the child as a complete, self-contained SD. From that point on, the access check on the child consults only the child's own SD; the parent is not walked.
This is the eager evaluation model. It has one large consequence: modifying a parent's inheritable ACEs does not propagate to existing children. The children's SDs were computed when they were created and are now their own. Whatever changes you make to the parent affect only objects created after the change. Children that already exist keep the SDs they had.
This page covers the inheritance algorithm — what counts as inheritable, how the merge works, the special placeholders (CREATOR_OWNER, CREATOR_GROUP), and the SE_DACL_PROTECTED and SE_SACL_PROTECTED flags that opt an object out of inheritance entirely.
Eager vs lazy #
Two models are possible:
- Lazy inheritance: a child has no inherited ACEs of its own; access checks on the child walk the parent SD too. Changes to the parent propagate immediately.
- Eager inheritance: a child's SD is fully computed at creation; access checks consult only the child. Changes to the parent affect future children only.
Peios uses eager inheritance, like the access-control model it grew out of. The advantages: access checks are local (you only need one SD's worth of data), no walking up directory trees during evaluation, no concurrency issues with "the parent moved between my permission check and the operation". The disadvantage: when you change a parent's permissions, you need a separate sweep to propagate the change to existing children — a tool's job, not the kernel's.
In practice, eager inheritance means: when an administrator changes a directory's DACL and wants the change to apply to existing files, they need a propagation tool. The kernel does not do this automatically and provides no syscall for "re-inherit all children". A propagation tool walks the tree and rewrites child SDs explicitly.
The inheritance flags #
Four ACE flags control how an ACE propagates to children. They are the same four mentioned on the ACLs and ACEs page:
| Flag | Effect at creation of a child |
|---|---|
OBJECT_INHERIT_ACE (OI, 0x01) | The ACE inherits to children that are non-containers (files, not directories). |
CONTAINER_INHERIT_ACE (CI, 0x02) | The ACE inherits to children that are containers (directories). |
NO_PROPAGATE_INHERIT_ACE (NP, 0x04) | The ACE inherits to direct children but its OI, CI and NP flags are cleared in the inherited copy, so it stops propagating after one level. |
INHERIT_ONLY_ACE (IO, 0x08) | The ACE is not evaluated on the object it sits on. It exists only to be inherited. |
Common combinations:
| Flags | Meaning |
|---|---|
CI | OI | Inherit to all descendants, files and directories, recursively. The most common pattern. |
CI | Inherit to descendant directories only. |
OI | Inherit to descendant files only. |
CI | OI | IO | Apply to descendants but not to this object. |
CI | OI | NP | Apply to immediate children only. Grandchildren do not inherit. |
(none) | This ACE does not inherit. It applies only to this object. |
The INHERIT_ONLY_ACE (IO) flag is what lets you say "this rule is for children, not for the parent itself". An IO ACE is invisible to the access check on its own object; the DACL walk skips it. But it is copied to children at creation time.
The merge algorithm #
When a new child is created, the kernel needs to produce the child's SD from three inputs:
- The parent's SD. Inheritable ACEs from here will be copied (with adjustments) into the child.
- The creator's SD, if one was supplied as a parameter to the create call. This contains explicit ACEs the creator wants on the new object.
- The creator's token. Provides defaults (owner, primary group, default DACL) when the creator did not supply specific values.
The merge proceeds as follows:
- Compute the child's owner. If the creator supplied an owner in the explicit SD, use it (subject to the same-self-or-owner-group rule from Ownership). Otherwise, use the creator's token's default owner.
- Compute the child's primary group. Same rule: explicit SD if present, otherwise the creator's token's default primary group.
- Compute the child's DACL. If the creator supplied a DACL, start with its explicit ACEs and append the inheritable ACEs from the parent's DACL — only those whose flags say they should propagate to this kind of child (container or non-container). If the creator supplied no DACL, the child's DACL is the inherited ACEs alone. Only when the parent yields no inheritable ACE at all is the creator's token consulted: the child then gets the token's
default_dacl, and if the token has none, a null DACL. The default DACL is a fallback, never a base that inherited ACEs are added to. - Compute the child's SACL. Same algorithm as the DACL, applied to SACL ACEs.
Each inherited ACE goes through a small transformation as it is copied:
- The
INHERITED_ACEflag (0x10) is set in the copy. This marks it as having come from inheritance, not from explicit assignment. - If
NO_PROPAGATE_INHERIT_ACEwas set, theOI,CIandNPflags are cleared in the copy. - If the ACE's SID is
CREATOR_OWNER(S-1-3-0), the SID is substituted with the new child's owner SID. - If the ACE's SID is
CREATOR_GROUP(S-1-3-1), the SID is substituted with the new child's primary group SID.
The CREATOR substitution is the mechanism that makes inheritable ACEs portable. A directory's DACL containing CI | OI ACCESS_ALLOWED CREATOR_OWNER GENERIC_ALL means "every file or subdirectory created under here grants full access to whoever owns it". The owner of each child is different; the substitution at creation time fills in the right SID for each one.
Creator SIDs and generic rights can produce two ACEs #
Substitution alone would answer the question for one object and destroy the rule that asked it. A subdirectory would come away holding a concrete SID — its own creator's — with that access over everything beneath it, permanently, and nothing further down would ever resolve against its own creator again.
When a container inherits an ACE that names a creator SID or carries generic rights, and the ACE both applies to the child and propagates further, it receives both halves:
| SID and rights | Flags | |
|---|---|---|
| The rule's answer for this container | creator SID substituted, generic rights mapped | INHERITED, no inheritance flags |
| The rule, carried onward | original SID and generic mask | original propagation flags, plus IO and INHERITED |
The second is inherit-only, so it grants nothing on the directory itself; it exists to be inherited again further down, where it resolves against whoever creates the next object.
This also applies to registry keys. A generic-rights ACE with an ordinary SID still needs both copies: its effective rights are mapped now, while its original generic mask is preserved for descendants. An ACE with a concrete SID and concrete rights is inherited as one copy.
An ACE that targets files only (OI without CI) passes through a directory as one unresolved, inherit-only copy. Adding NP prevents that copy from reaching files below the directory, so the directory inherits no such ACE.
A non-container receives the resolved ACE alone. A file has no children for the rule to reach, so the copy carries no inheritance flags either.
NO_PROPAGATE_INHERIT_ACE ends the rule as it ends any other: with OI, CI and NP cleared there is nothing to carry, and only the substituted ACE is written.
Sources of an inherited SD, ranked #
Which source wins for each component of the child SD, in order:
| Component | First source | Second source | Third source |
|---|---|---|---|
| Owner | Creator's explicit SD (if owner present) | Creator's token's default owner | (none — must be present) |
| Primary group | Creator's explicit SD (if group present) | Creator's token's default primary group | (none — must be present) |
| DACL | Creator's explicit SD (if DACL present), plus the parent's inheritable ACEs | The parent's inheritable ACEs alone | Creator's token's default_dacl, and a NULL DACL if the token has none |
| SACL | Creator's explicit SD (if SACL present), plus the parent's inheritable ACEs | The parent's inheritable ACEs alone | (no SACL means no audit policy) |
Whenever the parent has inheritable ACEs, they are appended to an explicit DACL or SACL rather than replacing it, so a creator who supplies an explicit DACL gets that DACL plus the inheritable ACEs from the parent. They cannot exclude the parent's ACEs except by setting the protected flag (see below).
The token's default_dacl is a fallback, not a base. It is consulted only when the creator supplied no DACL and inheritance produced nothing, which depends on the filesystem and its explicit subtree policies. The cases that reach it are objects with no parent to inherit from, such as an abstract socket, and children of a container whose descriptor was deliberately written without inheritable ACEs.
Protected ACLs #
There is an opt-out: the SE_DACL_PROTECTED and SE_SACL_PROTECTED flags in the SD's control field.
| Flag | Effect |
|---|---|
SE_DACL_PROTECTED (0x1000) | The child's DACL does not accept inheritable ACEs from the parent. Only the explicit ACEs in the creator's SD appear. |
SE_SACL_PROTECTED (0x2000) | Same, for the SACL. |
When a creator sets these flags on the explicit SD they pass to the create call, the resulting child's DACL/SACL is purely what the creator wrote. The parent might have a hundred inheritable ACEs; none of them appear on the child.
Protected ACLs are used when an object should be administered without reference to its container. A service that creates files whose access should be controlled only by the service, not by whoever happens to own the directory it lives in, sets SE_DACL_PROTECTED on each file's explicit SD at creation.
The flags can also be set after creation, via a subsequent kacs_set_sd. Doing so does not retroactively remove previously inherited ACEs — they were copied at creation time and are now the child's own — but it does protect against future re-inheritance if a propagation tool sweeps the tree.
A protected DACL is how a subtree gets its own policy #
The Peios filesystem is a worked example, and a useful one because it is small enough to hold in your head.
dev.peios.fsbase declares protected inheritance scopes for the intended
public software trees: /usr, and /lcl/bin, /lcl/sbin, /lcl/libexec,
/lcl/lib, /lcl/include, /lcl/share, /lcl/etc and /lcl/conf.
SYSTEM and Administrators have full control; Everyone inherits read/execute.
Thus newly installed software stays readable to ordinary principals, including
on a live image whose lower filesystem synthesises non-inheritable descriptors.
This is not a public inheritance policy for all of /var.
| Path | Protected policy |
|---|---|
/home | SYSTEM and Administrators can list it; Everyone can traverse but cannot list. Individual homes receive their own principal-specific descriptor. |
/tmp, /var/tmp | Everyone can create entries but cannot delete another principal's entries. CREATOR OWNER inheritance makes children private to their creator, SYSTEM and Administrators. /var/tmp retains its persistent semantics. |
/var/state/secrets | SYSTEM-only, inherited by children. Other consumers require explicit grants. |
A protected parent does not retroactively protect an existing child. Runtime
credential writers must establish protection before writing; a later rename
or a Unix mode such as 0600 does not replace the Peios security descriptor.
Auto-inherit flags #
Two more SD control flags work alongside the protected flags:
| Flag | Effect |
|---|---|
SE_DACL_AUTO_INHERIT_REQ (0x0100) | Set by a creator that wants the child to participate in auto-inheritance. Tools that propagate inherited ACEs use this to decide whether to update the child. |
SE_DACL_AUTO_INHERITED (0x0400) | Set by the kernel on a child whose inherited ACEs have been computed by the auto-inheritance algorithm. |
The corresponding SACL pair (SE_SACL_AUTO_INHERIT_REQ, SE_SACL_AUTO_INHERITED) works the same way.
These flags exist so tools can distinguish "this child wants automated propagation" from "this child has its own bespoke DACL the user wrote by hand". A propagation tool sweeping a tree typically updates only children with the auto-inherit flags set, leaving hand-crafted DACLs untouched.
What inheritance does not do #
A handful of things often attributed to inheritance but not done by it:
- Inheritance does not happen at access-check time. Once a child has its SD, the parent is not consulted on access decisions. Inheritance is purely a creation-time activity.
- Inheritance does not retroactively update children. A change to a parent's inheritable ACEs is invisible to children created before the change. Propagation requires an explicit sweep.
- Inheritance does not chain across moves. Moving a file from directory A to directory B does not re-run inheritance against B. The file's SD remains whatever it was. (Copying is a different operation; it creates a new object, which does go through inheritance.)
- Inheritance does not preserve ACE order. Inheritable ACEs from the parent are appended to the explicit ACEs from the creator. If you want canonical order, the creator needs to either start from the parent's order and intersperse, or run a canonicalisation pass afterwards.
Inheritance and creator-supplied SDs #
A subtle interaction worth knowing: when a creator passes an explicit SD to the create call, the explicit ACEs go before the inherited ones. This means the creator's ACEs sit higher in the DACL than the parent's, and first-writer-wins favours them.
If the parent says "deny X" and the creator says "allow X", the creator's allow wins because it sits first. If the canonical order says the parent's inherited deny should win (because explicit ACEs from the creator should not override the parent's policy), the creator needs to either not supply a competing ACE or arrange the explicit SD to use the protected-ACL flag.
This is one of the reasons creators usually pass no explicit DACL (relying on the default-DACL + parent-inheritance merge) rather than constructing a custom SD. The latter is correct in cases where the creator knows exactly what the child's policy should be; the former is correct in cases where the creator just wants the natural policy of its location.
Where to go next #
For ACEs whose effect is gated by an expression rather than fixed at write time, read Conditional ACEs.
To sweep a tree and propagate inheritable ACEs from a shell, read The sd command.
Private state and data defaults #
The base filesystem gives /var/state, /var/cache, /var/log, /var/crash,
/data and /srv protected, inheritable SYSTEM/Administrators full access.
Everyone may traverse each container to an explicitly shared child, but may not
list it, create entries, or inherit public access into new contents. The reduced
initramfs skeleton carries the same policies for its /var branches.
A package or administrator must establish an explicit child scope before a
service writes there: grant the service's own SID and any intended readers.
Changing a directory descriptor does not recursively rewrite existing children.
Public software under /usr and the designated /lcl branches remains readable;
/var/tmp keeps its separate creator-private temporary-file policy. The secrets
scope stays SYSTEM-only, excluding Administrators unless explicitly granted.
SYSTEM remains trusted across private scopes. A service-specific grant isolates a lower-privilege service from unrelated stores; it does not contain another process that retains SYSTEM identity or equivalent machine-wide privileges.
The credential store under /var/state/lpsd grants the distinct lpsd service
account and SYSTEM; it continues to exclude Administrators and other services.