File access
Single-page view · as markdown
File access
Peios / Peios Security Fundamentals / File access
FACS — the File Access Control Shim — is the kernel layer that applies KACS access control to files. Where the kernel's access pipeline runs against any protected object, FACS is the specific bridge between the file system (inode, dentry, page cache) and the access check. Every read, write, open, and metadata operation on a file goes through FACS at some level; FACS decides whether the operation proceeds and, when relevant, what flags or rights apply.
The model FACS uses is the handle model: AccessCheck runs once at open time, the granted mask is cached on the file descriptor that comes back, and every subsequent operation through that fd reads from the cache rather than re-running the access check. The implications of this single design choice ripple through every page in this topic.
This page covers the model at a conceptual level. The rest of this topic explains the handle model, how to inspect and make bounded changes to file security, and the special cases that affect diagnosis. The syscall and SDK details live in the technical references linked below.
The handle model in one sentence #
Access is decided at the moment a file descriptor is opened; from then on, the descriptor carries an immutable "granted access mask" that gates every operation.
That sentence is the whole model. Once a file is open, the kernel knows what the caller is allowed to do with this file via this handle. Reads, writes, metadata queries, mmaps — each operation has a required access mask (what it needs to do its work) and succeeds only if the required mask is a subset of the cached granted mask.
The mask does not change. Adjustments to the file's DACL, to the calling token, or to anything else after the open do not affect the cached mask. The handle is a snapshot of what the access check said at open time.
The full mechanics — what gets cached, what operations consult the cache, what is and is not subject to the model — are in The handle model.
Why the handle model #
Two main reasons:
Performance. Re-running AccessCheck on every read or write would be expensive. The DACL walk plus narrowing layers plus possibly CAAP evaluation is non-trivial work. Caching the result on the handle reduces operations to a single bitmask comparison — orders of magnitude cheaper.
Consistency. A long-running operation should not change behaviour halfway through because the DACL was modified. A file being read for backup should not start failing midway because an administrator tightened the DACL; either the backup tool has the right at the start (in which case it should complete) or it doesn't (in which case it shouldn't have been able to open in the first place).
The trade-off is that an open handle survives a policy change. If you tighten a file's DACL, processes that already have the file open keep their existing access. New opens get the new policy; old handles are unchanged.
This is the check-at-open principle — covered for CAAP in Central access policies and applied uniformly here. The kernel does not retroactively revoke open handles.
Where FACS sits #
FACS lives between the file system and the rest of KACS. The arrangement:
flowchart LR
A["Userspace syscall (open, read, write)"] --> B["FACS"]
B -->|at open| C["AccessCheck"]
B -->|cached mask on fd| D["File operation"]
C --> D
D -->|persisted| E["Filesystem (ext4, xfs, etc.)"]
At open, FACS calls AccessCheck and caches the result on the fd. At every subsequent operation, FACS reads the cached mask and decides. The filesystem itself doesn't see KACS; it sees only operations that FACS has already vetted.
This means:
- The DACL and the filesystem are decoupled. FACS is what enforces KACS on files; the filesystem stores the SD as metadata (an xattr, typically) but does not interpret it.
- A filesystem can be FACS-managed or not. Mount policy determines whether FACS applies to a given mount; see Mount policies.
- Different filesystems can store SDs differently. ext4, XFS, NTFS — each has its own xattr or native mechanism for persisting the SD. FACS handles the abstraction; the access check works the same way regardless.
Two open syscalls #
The kernel exposes two ways to open a file:
| Syscall | Mode | Common use |
|---|---|---|
kacs_open | KACS-native — caller specifies an explicit desired access mask | New code; programmatic file access by services |
openat / open | Legacy — caller specifies POSIX flags (O_RDONLY, O_WRONLY, etc.) that map to access masks | Existing Linux applications |
kacs_open is the native interface. In ordinary strict mode, the caller specifies which rights it wants and either gets all of them or fails. openat and friends are the POSIX-compatibility interface — they use the same FACS machinery internally but with a different mapping from input flags to access mask and with split "core" and "compat" semantics for handling partial grants.
Both paths converge in FACS. The cached mask on the resulting fd is the same shape regardless of which syscall produced the open.
For an operator, the important distinction is between a successful open and a successful later operation: that operation may need a right the handle did not receive. Diagnose the actual caller and operation with Debugging a denial, and use Managing file security for a deliberate policy change.
The programmer-facing kernel references are KACS-Native Open and Legacy Open Compatibility. Their exact masks and edge cases have documented discrepancies; this overview does not resolve them. Native creation can also accept a caller-supplied SD; see creation rules and Inheritance for the distinction from inherited policy.
What FACS reads from where #
A summary of the data sources that feed into a FACS access check at open:
| Source | What it provides |
|---|---|
| Calling thread's effective token | Identity for AccessCheck — user SID, groups, integrity, privileges, etc. |
| File's inode | The owner SID, primary group, DACL, SACL (read via xattr or filesystem-native channel) |
| Mount policy | Whether FACS applies at all, how missing SDs are handled, the mount-level SD template |
| Process PSB | PIP fields for the dominance check during AccessCheck |
Caller's kacs_open parameters | The desired access mask, the create disposition, optionally a creator-supplied SD |
These come together at open. AccessCheck runs over them and produces the granted mask that goes on the fd.
What FACS does not do #
A few clarifications:
- FACS does not store SDs. The filesystem stores SDs; FACS reads them. Different filesystems use different mechanisms (xattrs on ext4 / XFS / Btrfs, native security streams on NTFS, mount-level synthesis on FAT/exFAT, in-memory on tmpfs).
- FACS does not validate SDs at boot. The first access to a file is when its SD is first read and validated; the kernel does not pre-scan filesystems looking for problems.
- FACS does not propagate DACL changes. Modifying a file's DACL affects future opens, not existing handles. This is the check-at-open rule.
- FACS does not gate operations that bypass the file system. A process that has a file mapped into memory via mmap can read or write that memory without going through FACS for each access — the access check happened at mmap time (or at open, then at mmap), and after that the kernel has no efficient way to intercept memory accesses. Mitigations like LSV apply at the mmap call; runtime memory access is governed by the page-table permissions the kernel set when the mapping was created.
Where to start #
If you want the handle model in detail — what is cached, what operations check the cache, when the cache can be stale, how fd transfer works — read The handle model.
For programming, use the SDK opening reference, the native and legacy kernel references, and the generated KACS ABI. Read the open-interface discrepancy note alongside them.
To inspect a file's policy, make a bounded change with sd, and verify the original operation, read Managing file security.
If you want the edge cases — O_PATH, the exec dual gate, append-only files, sticky bit, POSIX ACLs that no longer work, NFS dual authority — read Special cases.
The handle model
Peios / Peios Security Fundamentals / File access
The handle model is the rule that an open file descriptor carries a fixed snapshot of access permissions, taken at the moment of open. Every operation through the fd consults this snapshot — not the file's current SD, not the calling token's current state, not anything else. The snapshot is the granted access mask, an immutable 32-bit value attached to the fd.
This page covers what the snapshot contains, which operations consult it, why immutability is the right choice, and the implications for handle transfer between processes.
What is on the fd #
When open succeeds, the kernel attaches several things to the resulting fd. The KACS-relevant pieces:
| Field | Set at | Mutable after open? |
|---|---|---|
| Granted access mask | AccessCheck at open | No |
| Continuous audit mask | AccessCheck at open (if alarm ACEs matched) | No |
| FACS-managed flag | Open path | No |
| (Filesystem state: position, mode, etc.) | open | Mutable per operation |
The granted access mask is the central object. It is whatever AccessCheck decided the caller's effective token was entitled to on this file at this moment. The bits set are the rights the fd holder has; the bits not set are rights they do not have.
The continuous audit mask is the union of all alarm ACEs that matched at open time. Every subsequent operation through the fd will check against this mask and emit a kacs.audit.handle.used event if the operation's required mask overlaps it. See The SACL and Audit ACEs.
The FACS-managed flag distinguishes fds that go through the handle model from those that don't. O_PATH fds, for example, are not FACS-managed (covered in Special cases).
What operations consult the cache #
Almost every operation on a FACS-managed fd checks the cached mask. The rule: the operation has a required access mask (what it needs the caller to have been granted at open) and succeeds only if required & ~granted == 0 — every required bit is in the granted mask.
| Operation | Required access |
|---|---|
read | FILE_READ_DATA |
pread, readv, process_vm_readv (when reading from the fd as target) | Same |
write | FILE_WRITE_DATA |
pwrite, writev | Same |
| Append-only write (RWF_APPEND, O_APPEND) | FILE_APPEND_DATA (without requiring FILE_WRITE_DATA) |
ftruncate | FILE_WRITE_DATA |
fchmod | FILE_WRITE_ATTRIBUTES |
fchown | WRITE_OWNER (updates only the inert Linux uid/gid; see Special cases) |
mmap(PROT_READ) | FILE_READ_DATA |
mmap(PROT_WRITE) | FILE_WRITE_DATA |
mmap(PROT_EXEC) | FILE_EXECUTE |
fstat | FILE_READ_ATTRIBUTES |
fgetxattr | FILE_READ_EA (for ordinary xattrs; the security namespace is unconditionally denied) |
fsetxattr | FILE_WRITE_EA (same exception) |
fdatasync, fsync | None — these are control operations, not data operations |
flock, fcntl(F_SETLK) | None |
Each operation knows what it requires; the kernel compares against the cached mask and decides.
A subtle case: the operation's required mask comes from the operation's semantics, not from POSIX flags. A read() on a fd opened O_RDONLY needs FILE_READ_DATA because read needs FILE_READ_DATA, not because the fd was opened with O_RDONLY. The two coincide because open(O_RDONLY) requested and got FILE_READ_DATA, but the per-operation check is on what the operation needs, not on what the open requested.
Immutability and why #
The cached granted mask cannot change after open. The kernel does not provide a syscall to refresh it. Modifying the file's DACL does not propagate to existing fds. Adjusting the calling token's privileges does not retroactively re-grant rights on previously-opened fds.
The reasoning is two-fold:
Atomic policy. A long-running operation should not change behaviour partway through. A backup tool that opens a file at time T should be able to complete the read at time T+1 even if the DACL was modified between. Either the right to read was granted at open (in which case the read should succeed) or it wasn't (in which case the open should have failed).
Performance. Caching is the whole point. If the kernel re-evaluated AccessCheck on every read, the handle model would not give any of the performance benefit. The cache is what makes the model work.
The implication: a DACL update is observable only to new opens. Existing handles continue with the rights they had. To force a policy change to take effect, the operator must either close existing handles (or kill the processes holding them) or wait for the handles to be released naturally.
fd transfer preserves the mask #
A file descriptor can be transferred between processes — via dup, fork, SCM_RIGHTS, exec, pidfd_getfd. In every case, the transfer preserves the granted access mask exactly.
A fd passed via SCM_RIGHTS to another process gives that process the same access the originating process had through the fd. The recipient's token is not re-checked at transfer; the granted mask is what counts.
This is intentional. Capabilities are passable. A process that opened a file with broad rights can hand the fd to a helper process; the helper inherits the rights even if the helper's own token would not have granted them.
The model is: opening a file produces a capability (the fd + the cached mask), and capabilities are transferable. The transfer is the way the model expects rights to be delegated.
The corollary: a process should be careful about which fds it shares. Passing a fd to a less-trusted helper grants that helper the rights the fd carries. There is no narrowing-at-transfer mechanism.
A more restrictive variant — handing off only some of the access rights — requires opening a fresh fd with the narrower set. The original fd holder, who presumably has the broader set, can open the file again with a narrower mask and pass that fd.
fork and exec #
fork copies the file table; all fds are inherited by the child along with their cached masks. The child has the same rights on the same files as the parent at the moment of fork.
exec (with no special flags) preserves the file table; fds carry over to the new program with their cached masks intact.
exec with FD_CLOEXEC set on a fd causes that fd to be closed at exec; the child program does not inherit it. This is the standard "close on exec" mechanism, used widely. A handle that should not survive exec gets FD_CLOEXEC either at open (via O_CLOEXEC) or later (via fcntl(F_SETFD, FD_CLOEXEC)).
Within the same process, threads share the file table; a fd opened in one thread is usable from any thread in the same process.
The cached mask follows the fd everywhere the fd goes.
When the cache can be wrong #
The cached mask is a snapshot at open time. There are a small number of cases where the cache may be wrong relative to the world at large:
- The DACL was changed after open. The cache reflects the DACL at open; the on-disk DACL may now grant different rights. New opens would get the new rights; this fd still has the old. Not a bug — the model is check-at-open.
- The calling token's state changed. The token's privileges may have been adjusted, the integrity level may have changed (via NEW_PROCESS_MIN at exec, say). The cached mask reflects the token at open. Operations through this fd use the cached value, not the current token state.
- The file was deleted and replaced. An
unlinkof the path the fd was opened against does not affect the fd; the inode remains alive as long as the fd is open. The cache still reflects what was true of the now-unlinked inode. A new file created at the same path is a different inode; the cache is unaffected. - Mount policy changed. Changing the mount policy on a superblock does not retroactively affect the cached masks of fds open against files on that superblock. The cache is the snapshot; mount policy is the input to future opens.
In each case, the model's answer is the same: the cache is the truth for this fd's purposes. If you need a fresh view, close and reopen.
Operations not subject to the cache #
A handful of operations bypass the cache or use a different check:
execveat(AT_EMPTY_PATH)is the one exception in v0.20. Exec re-evaluates AccessCheck rather than using the cache. The reasoning is that exec changes the calling process's identity and PSB in ways that the cached open-time decision did not account for; running a fresh check makes the exec decision honest. This is the only v0.20 use-time access check.- O_PATH fds are not FACS-managed and have no cached mask. Operations against them either work unconditionally (
fstat) or use a fresh AccessCheck (operations that need real access). - Mapping a file's data into kernel space directly (e.g. via
splicebetween two fds) involves both fds' cached masks; neither is bypassed.
The exception list is short. For the overwhelming majority of operations, the cached mask is the gate.
Implications for processes #
Knowing the handle model has practical implications for code that handles files:
Open with exactly the rights you need. A fd opened with broader rights than needed is a broader capability than needed. If something might pass the fd elsewhere, the something gets the broader rights too. Minimum-rights opens are good hygiene.
Pass fds carefully. A fd shared via SCM_RIGHTS is a capability transfer. Audit who you pass to and what they could do with it.
Use FD_CLOEXEC for fds you don't want exec to inherit. This is also a defence-in-depth measure against accidental capability leakage to child processes.
Refreshing access requires reopening. A program that wants to see DACL changes needs to close and reopen, not poll for changes.
Where to go next #
For the interfaces that establish the granted mask, see the programmer-facing native-open and legacy-open kernel references, with their unresolved documentation differences.
For reading and writing the SD behind a handle, read Managing file security.
For the edges of the model — O_PATH, the exec dual gate, append-only handles — read Special cases.
Special cases
Peios / Peios Security Fundamentals / File access
The handle model handles most of file access cleanly. But files have edges — operations that bypass the model, semantics that don't fit, filesystems whose behaviour FACS cannot fully control. This page covers those edges.
Each of the special cases below is something that someone debugging FACS behaviour will eventually hit. Knowing the rules ahead of time keeps the gotchas from being surprises.
O_PATH — fds without a cached mask #
O_PATH is a Linux flag for opening a file in a "path-only" mode. The fd that comes back can be used to refer to the file (for openat(dirfd, ..., 0, fd), for fstatat, for path manipulation) but cannot be used for actual data operations.
Under FACS, an O_PATH open does not run a full AccessCheck. The fd is not FACS-managed; it has no cached granted mask. Operations that would normally consult the cache either work unconditionally (because they don't need any access) or use a fresh live AccessCheck (because they do):
| Operation on O_PATH fd | Behaviour |
|---|---|
fstat, fstatat | Works without access check. The fd is a path reference; getting stat info is allowed. |
openat using the fd as dirfd | Normal — runs AccessCheck for the new open. The dirfd's O_PATH status doesn't affect the new fd. |
kacs_get_sd with AT_EMPTY_PATH | Runs a live AccessCheck (the fd has no cached mask to consult). |
kacs_set_sd with AT_EMPTY_PATH | Same — live AccessCheck. |
read, write, mmap | Denied with EBADF. The fd cannot be used for data operations. |
fchmod, fchown, fgetxattr, fsetxattr, ioctl | Denied with EBADF. |
O_PATH is useful for path manipulation — keeping a reference to a directory while traversing the tree, or referring to a file by fd rather than by path. For these uses, the lack of a cached mask is irrelevant.
The catch: a process that wants to do anything with an O_PATH fd beyond path navigation needs to reopen it. The reopen is a fresh access check that decides what the resulting fd can actually do.
The exec dual gate #
Exec is the only operation in v0.20 that performs a use-time access check rather than relying on the cached mask. The reasoning: exec changes the process's identity (via the new binary's PIP) and effectively replaces it; the open-time decision is no longer the right one to use.
Specifically, execveat(AT_EMPTY_PATH) on an open fd:
- Runs a fresh AccessCheck against the file's current DACL using the caller's current effective token.
- Requires both the
+xmode bit on the file andFILE_EXECUTEon the caller's access to be present.
The two checks answer two different questions:
+xon the file means this file is intended to be run as a program. It is a property of the file itself, set by whoever owns it — the file's author saying "this is an executable, not data". A file without+xis data; trying to exec it fails regardless of who is asking, because exec on data is meaningless.FILE_EXECUTEon the access means this caller is allowed to execute this program. It is an access decision against the DACL — the question of whether this principal, on this token, is authorised to run this binary.
Both have to be true for exec to proceed. A data file (no +x) is not runnable by anyone; an executable file the caller is not authorised for is runnable in principle but not by this caller.
The dual gate is the v0.20 exception to the handle model. The reason for live evaluation here is that exec's consequences are large enough — the binary that runs is what the kernel will then trust at the verified PIP level — that running on a stale cache is the wrong default.
mmap(PROT_EXEC) is different from exec. It uses the cached mask (FILE_EXECUTE from the open), and additionally runs the LSV mitigation if enabled. mmap is not the same as exec; the dual-gate-with-live-check applies only to exec specifically.
Append-only handles #
A handle opened with FILE_APPEND_DATA granted but not FILE_WRITE_DATA is an append-only handle. The kernel enforces this strictly:
write()at the current offset is allowed only if the current offset is at end-of-file. Otherwise denied.pwrite()at any offset other than end-of-file is denied (RWF_APPEND-style).- Mapping the file shared writable (
mmap(MAP_SHARED, PROT_WRITE)) is denied. ftruncateto extend the file is denied (cannot rewind via truncate).fallocatemodes that would mutate (FALLOC_FL_PUNCH_HOLE, FALLOC_FL_COLLAPSE_RANGE, FALLOC_FL_ZERO_RANGE) are denied.
Operations that legitimately only append — POSIX write with O_APPEND, RWF_APPEND writes — work normally. The kernel makes sure the data lands at end-of-file.
The use case: log files that should be append-only. A logger holds a handle with FILE_APPEND_DATA but not FILE_WRITE_DATA; it can write log lines but cannot rewrite earlier ones. Even if the logger is compromised, the existing log entries are safe from modification through this handle.
Append-only is the FACS expression of the "secure log" pattern. It exists at the right-mask level, not as a separate file attribute.
sticky bit — no effect under FACS #
The traditional Unix sticky bit on a directory restricts file deletion within: only the owner of a file (or the owner of the directory) may unlink files in a sticky-bit directory. This is what makes /tmp work.
Under FACS, the sticky bit has no effect. Deletion is gated by FILE_DELETE_CHILD on the parent directory's SD. The sticky bit's semantics are not encoded.
The reason: FACS's access model is comprehensive enough that the sticky bit is unnecessary. A /tmp directory's DACL grants the directory's FILE_DELETE_CHILD to the appropriate principals (typically just the owner or the file owner, mediated through OWNER RIGHTS); this is the same effect the sticky bit had, expressed in the DACL.
For /tmp specifically, the Peios-default DACL provides equivalent semantics: each user can create files in /tmp (FILE_ADD_FILE granted to Authenticated Users), each file's owner can delete their own (FILE_DELETE_CHILD inherited per-file from OWNER RIGHTS ACEs), but a user cannot delete another user's files.
A directory whose mode includes the sticky bit (visible to ls -l as a t) is informational; the mode is derived from the SD by FACS. The bit can be set but has no enforcement consequence.
POSIX ACLs — replaced by KACS #
The Linux POSIX ACLs (set via setfacl and stored as system.posix_acl_access and system.posix_acl_default xattrs) are not honoured by FACS. They are replaced by KACS DACLs.
Specifically:
- Writes to
system.posix_acl_accessandsystem.posix_acl_defaultxattrs are unconditionally denied. - Existing POSIX ACL xattrs on files (carried over from a pre-Peios system, say) are ignored. FACS uses the KACS DACL exclusively.
The kernel will not silently translate POSIX ACLs to KACS DACLs. Migrating from a Linux system with POSIX ACLs requires re-writing the SDs in KACS form. Tools that can do this conversion exist in the migration tooling; the kernel does not do it on the fly.
fchown — gated on WRITE_OWNER #
The legacy fchown syscall changes a file's owner UID/GID. Under FACS, the family is permitted but gated on SD rights:
fchown()succeeds only if the fd's granted mask includesWRITE_OWNER; otherwise-EACCES.chown()andlchown()run a fresh access check requiringWRITE_OWNERon the file's SD.
The Linux uid/gid change does not alter the SD's owner SID. Changing the real owner goes through kacs_set_sd with OWNER_SECURITY_INFORMATION — the KACS semantics for ownership (covered in Managing file security) replace the legacy mode-and-owner duo.
The kernel surfaces the file's owner SID's projected UID as the result of stat-style queries, so ls -l shows a UID. But the UID is derived from the owner SID; setting it via fchown is not the way.
NFS — dual authority #
NFS client mounts are a special case. The underlying filesystem is on a remote server; the server enforces its own access control independent of what the client thinks. FACS on the client side cannot reach into the server's authorisation; it can only express its local view.
The pattern Peios uses: NFS client mounts are configured with the synthesize_ephemeral mount policy. The client synthesises a local SD for each file accessed (per the mount template), runs AccessCheck locally, and either authorises the operation or denies it. If authorised, the operation is forwarded to the server, which may then deny it for its own reasons.
The consequence: a locally-authorised open can still produce I/O errors when the server refuses. A caller may successfully open(O_RDONLY) against an NFS file (FACS's local synthesis decided to grant) but the read() returns -EACCES from the server.
This is "dual authority": the client and the server both have a say, and both have to permit. The client's denial is final on the client side; the server's denial is final on the server side. There is no single source of truth for what is allowed.
Other implications:
- Expect I/O errors from server-side denial. A successful open does not guarantee a successful read. Handle
EACCES(orEIO) from the operation, not just from the open. - The synthesised SD is local. Changes to the file's actual SD on the server are not visible to FACS's local synthesis. Tools that want to inspect the real SD need to query the server.
NFS server mounts (where Peios is the server) work the other direction: FACS enforces the SD on the local files; the protocol exposes it. The remote client's view of what is allowed is whatever the protocol negotiates, which may be limited by NFS-protocol-level restrictions but is ultimately decided by FACS.
/proc and /sys #
/proc and /sys are kernel-managed pseudo-filesystems. They are not FACS-managed in the standard sense — their mount policy is unmanaged, which means the FACS handle model does not apply.
What does happen:
- File operations under
/proc/<pid>/*route through the process's PSB / process SD checks (the two-check rule from PIP). Access is decided per-operation against the target process. /sys/kernel/security/kacs/*files have explicit SDs on them (set up by the kernel) and are accessed via the kernel's own check logic.- General
/procfiles (/proc/uptime,/proc/cpuinfo) are world-readable by convention. /syswrites are restricted toBUILTIN\AdministratorsandSYSTEMby hardcoded rule.
The result: operations on /proc and /sys work, but they don't go through the FACS handle model. The cached mask on the fd from opening one of these files is meaningless; the per-operation check is what gates access.
This is why cat /proc/self/status produces output without any apparent FACS involvement — the access is granted by the kernel's /proc-specific rules, and the kernel just makes data available.
The mount-policy classes are covered in Mount policies. unmanaged is the special class for kernel-managed filesystems; it cannot be set via the public ABI.
Whiteouts in renameat2 #
renameat2(RENAME_WHITEOUT) — the Linux-specific flag for creating a "whiteout" entry as part of a rename (used by overlay filesystems) — is supported on FACS-managed filesystems. The rename and the whiteout it leaves behind happen as one atomic operation, exactly as on stock Linux.
A whiteout is a chrdev(0,0) sentinel that overlay filesystems drop at the old name to mask a file in a lower layer. FACS authorises it the same way it authorises any new node: you need FILE_ADD_FILE on the source directory (where the whiteout lands), in addition to the usual rename rights. If you lack that right the whole rename is denied before anything is created, and the whiteout — like every freshly created node — inherits a security descriptor from its parent directory and is recorded in the audit trail.
This matters if you run overlayfs or union-style filesystems on FACS-managed storage: they work without the partial-support caveats earlier previews carried.
Summary table #
The special cases at a glance:
| Case | What is different |
|---|---|
| O_PATH | No cached mask; data operations denied; live check for SD ops |
| exec via execveat | Live AccessCheck plus Linux +x mode bit |
| Append-only handles | Write-at-end only; mmap-shared-writable and truncate denied |
| Sticky bit | No effect — the DACL is the gate |
| POSIX ACLs | Replaced by KACS; xattr writes denied |
| fchown / fchmod | Gated on WRITE_OWNER / WRITE_DAC; the mode bits are inert — the SD is the truth |
| NFS client | Dual authority; local FACS plus remote enforcement |
/proc and /sys | Unmanaged mount policy; per-operation check by kernel-specific rules |
| renameat2 RENAME_WHITEOUT | Supported; atomic, needs FILE_ADD_FILE on the source directory, whiteout inherits an SD |
Each is a deliberate decision. Some reflect Linux compatibility (POSIX ACLs replaced); some reflect security policy (exec dual gate, append-only enforcement); some reflect the model's limits (NFS dual authority). Knowing them ahead of time keeps them from being surprises.
Where to go next #
For the model these cases are edges of, read The handle model.
For how a mount's policy decides whether FACS applies at all, read Mount policies.
For the SD read/write syscalls referenced throughout, read Managing file security.