# Mount policies

---

# Mount policies

_Peios / Using Peios / Mount policies_

> Every mounted filesystem carries a mount policy — one per-superblock setting deciding whether FACS applies and how missing SDs are handled.

A **mount policy** is the per-superblock setting that controls how FACS treats a filesystem. The policy tells the kernel: does FACS apply here at all? When a file has no SD, what should happen? When the kernel synthesises a default SD because the file doesn't have one, where does the template come from? These questions are uniform within a filesystem — every file on a single mount gets the same treatment — and the answer is the mount policy.

There are four policy classes in v0.20. Three are FACS-managed (the kernel applies KACS access control); one is unmanaged (FACS doesn't apply at all). The class is decided when the filesystem is mounted, and changes apply lazily — existing handles are unaffected, but the next access against any file on the mount sees the new policy.

This page introduces the four classes at a glance. [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) covers each in detail with the missing-SD and corrupt-SD rules; [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) covers how each filesystem type physically stores the SD; [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md) covers the syscalls and the generation counter for cache invalidation.

## Why mount policies exist

A FACS-managed file needs a security descriptor. The DACL on it is the access decision input. The kernel needs to be able to look at any FACS-managed file and find an SD.

But not every filesystem can store an SD on every file. ext4 stores SDs in xattrs, but only up to 4 KB without special features; XFS supports xattrs natively up to 64 KB; tmpfs has no persistent storage at all; FAT and exFAT have no xattr support; remote filesystems like NFS may not surface SDs to the client. The kernel needs different behaviours for these cases.

That's what mount policies provide. The policy is the kernel's per-filesystem answer to "what to do when a file does not have an SD I can read":

- For some filesystems, refuse access (the file should have an SD and doesn't, so something is wrong).
- For some, synthesise an SD on the fly in memory (and don't try to write it back).
- For some, synthesise an SD and try to write it back so future accesses don't need to re-synthesise.
- For some (the kernel's own pseudo-filesystems), don't apply FACS at all.

Each of these is one of the four classes. The class is a property of the mount, not the file.

## The four classes at a glance

| Class | What it does | Typical use |
|---|---|---|
| **`facs_deny_missing`** | FACS-managed. A file with no SD is unreachable — every access fails. | System mounts (root, `/home`, `/var`) where every file should have an SD. |
| **`facs_synthesize_ephemeral`** | FACS-managed. A missing SD is synthesised in memory but not written back. The next access re-synthesises. | Removable media, FAT/exFAT, NFS client mounts — filesystems that may not preserve xattrs cleanly or where modifying SDs is not appropriate. |
| **`facs_synthesize_persistent`** | FACS-managed. A missing SD is synthesised and immediately written back. Subsequent accesses use the stored SD. | Filesystems being adopted into Peios — a previously-unmanaged filesystem getting an SD on first access. |
| **`unmanaged`** | FACS does not apply. The kernel uses its own per-operation access rules for files under this mount. | `/proc`, `/sys` — kernel pseudo-filesystems with their own access semantics. |

`unmanaged` is not settable via the public ABI — only the kernel itself sets this class, at boot time, for the pseudo-filesystems it manages. The other three are administratively settable.

A mounted filesystem has exactly one of these classes at any time. Changing the class is a single operation that affects every file on the mount going forward (subject to the lazy-invalidation behaviour).

## Per-superblock, not per-path

A mount policy is per-**superblock**, not per-path or per-bind-mount. All paths visible through the same underlying filesystem instance share the same policy.

The implications:

- A bind mount of `/etc/peios` to `/old/etc/peios` does not let you set different policies for the two paths. They are the same superblock; one policy.
- A read-only bind mount and the underlying read-write mount of the same filesystem share the policy.
- Two separate mounts of the same filesystem (uncommon but possible in some namespacing setups) each have their own superblock instance and can have different policies.

The per-superblock rule keeps mount policy simple. The alternative — per-mount or per-path — would mean a single inode could be subject to different policies depending on how it was reached, which would be operationally confusing.

## What the policy decides

For a FACS-managed mount (any of the three managed classes), the policy decides three things:

1. **What happens when an inode has no SD attached.** Different classes handle this differently — deny, synthesise-in-memory, or synthesise-and-persist.
2. **The mount-level SD template.** Each FACS-managed mount can carry a default SD template, used when synthesising an SD for a file that has no parent-derivable SD (root of the filesystem, typically).
3. **Whether mutations to synthesised SDs are persisted.** Ephemeral synthesises in memory only; persistent writes back to the filesystem.

For the unmanaged class, the policy decides one thing: FACS doesn't apply. Files under this mount are governed by whatever per-operation rules the kernel has for that pseudo-filesystem.

## What the policy does not decide

A few clarifications:

- **The policy does not change what filesystem operations are available.** Read, write, mmap, all the usual operations work normally on every class. The policy affects how the *access check* runs, not what operations exist.
- **The policy does not control persistence of file data.** A file's contents are persisted (or not) by the underlying filesystem; the policy is only about SD persistence.
- **The policy does not affect already-open handles.** The handle model caches access masks on fds; changing the policy does not retroactively change cached masks. Future opens see the new policy.
- **The policy does not propagate inheritably.** Changing a parent's mount policy doesn't affect mounted-elsewhere child directories — each mount carries its own policy on its own superblock.

## Where to start

If you want each class in detail — what `facs_deny_missing` actually does at runtime, how `synthesize_ephemeral` works for FAT, what happens when a synthesised SD turns out to be problematic — read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

If you want to know how each filesystem stores SDs — ext4's `ea_inode`, XFS's native xattr support, NTFS via `system.ntfs_security`, the no-xattr-support filesystems — read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

If you want the operational story — `kacs_set_mount_policy`, `kacs_get_mount_policy`, the generation counter that makes invalidation lazy, why mount policy changes don't walk the filesystem — read [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

## The commands

The concept pages above describe the model; three command-line tools put it to work. [`mount`](/peios/using-peios/mount-policies/mount.md) attaches a filesystem to the mount tree and is where a KACS mount policy (and its optional SD template) is chosen at attach time — the one place the policy classes above meet a command line. [`umount`](/peios/using-peios/mount-policies/umount.md) detaches a filesystem again, by mount point or by source, with lazy, forced, recursive and all-targets variants. [`lsblk`](/peios/using-peios/mount-policies/lsblk.md) lists the block devices available to mount, with their filesystems and, like `ls -l`, their SD-derived owner and mode. Use these when you want the operational reference for a specific flag rather than the model behind it.

---

# Policy classes

_Peios / Using Peios / Mount policies_

> The four policy classes, the synthesis chain that produces an SD when one is missing (parent → mount template → fallback), and the universal corrupt-SD rule.

The four policy classes are the answer to one question: when FACS needs an SD for a file and the filesystem doesn't have one, what should happen?

The three managed classes — `facs_deny_missing`, `facs_synthesize_ephemeral`, `facs_synthesize_persistent` — answer the question in three different ways. The fourth, `unmanaged`, answers it by stepping out of the question entirely: FACS doesn't apply, so the question doesn't arise.

This page covers each class in detail, the synthesis chain that produces an SD when one is needed, and the universal corrupt-SD rule.

## facs_deny_missing

A FACS-managed mount where missing SDs are treated as an error. Every file on a `facs_deny_missing` mount must have a valid SD; one that does not is unreachable.

The runtime behaviour:

1. The kernel reads the file's SD from the filesystem (typically an xattr).
2. If no SD is present, the access fails. The kernel returns `-EACCES` for any operation that would require an SD-based access check.
3. The file is effectively unreadable, unwritable, undeletable, unmodifiable. Every access fails because there is no policy to evaluate against.

This is the strict mode. It is appropriate for filesystems where every file should have been provisioned with an SD — system mounts, application storage, anywhere the operator has set up the filesystem deliberately.

The defaults for Peios system mounts (the root filesystem, `/home`, `/var`) are `facs_deny_missing`. The image-build process ensures every file in the base image has an SD; subsequent file creation always inherits an SD from the parent. There is no path by which a file without an SD legitimately ends up on these filesystems.

If somehow a file does end up without an SD — a backup-restore tool that skipped xattrs, a misbehaving filesystem driver — that file is unreachable. The fix is to give it an SD (`kacs_set_sd`, requires WRITE_DAC, which the caller may not have if they cannot read the file).

Practical upshot: a `facs_deny_missing` mount is strict. Files without SDs stay unreachable until given one. This is the right setting for filesystems where you trust the provisioning.

## facs_synthesize_ephemeral

A FACS-managed mount where missing SDs are synthesised in memory, but **not written back** to the filesystem. The synthesised SD is used for the access check; it does not become part of the file's persistent state.

The runtime behaviour:

1. The kernel reads the file's SD from the filesystem.
2. If no SD is present, the kernel **synthesises** one using the synthesis chain (covered below).
3. The synthesised SD is used for the access check.
4. The synthesised SD is **not** written back to the filesystem. It exists only in the kernel's cache for the duration this inode is in memory.
5. On a subsequent open of the same file (after the inode has been evicted from cache, say), the SD is synthesised again. The same inputs produce the same output, so the same SD comes out — but the synthesis is repeated.

This class is appropriate for filesystems where you can't or don't want to write SDs:

- **Removable media** (USB drives, optical media). You don't want to modify the media just because you read a file on it.
- **FAT and exFAT.** These filesystems have no xattr support; the SD has nowhere to go even if you wanted to write it.
- **NFS client mounts.** The actual file lives on a remote server; modifying its SD via xattr would have unpredictable effects.
- **tmpfs and devtmpfs.** Per-instance pseudo-filesystems with no real persistence.

The synthesis-only-in-memory pattern lets FACS apply access control to these filesystems without changing their on-disk content. The trade-off is that the cost of synthesising is paid every time the inode is cold.

## facs_synthesize_persistent

A FACS-managed mount where missing SDs are synthesised **and** written back. Each file is synthesised once; from then on it has a real SD.

The runtime behaviour:

1. The kernel reads the file's SD from the filesystem.
2. If no SD is present, the kernel synthesises one using the synthesis chain.
3. The synthesised SD is used for the access check — immediately, from the kernel's cache.
4. The synthesised SD is **also** written back to the filesystem (typically as the SD xattr). The file now has a persistent SD.
5. On a subsequent open, the SD is read from storage — no re-synthesis needed.

The write-back in step 4 does not happen *during* the access — it is **deferred** until just after the triggering operation finishes (when the kernel can safely write without holding the locks the access is using). In practice the SD is on disk by the time the syscall that first touched the file returns to userspace, so for an observer it is effectively immediate.

Because the synthesised SD is a deterministic function of its inputs (the parent's SD, or the mount template), the on-disk copy is a *cache* of a value the kernel can always recompute. If the write-back is interrupted — the inode is evicted first, or the process exits in the gap — nothing is lost: the next access re-synthesises the identical SD and tries the write-back again. A file is never left with the *wrong* SD, only occasionally with the SD still in memory rather than on disk. (A mount-template change in that gap simply discards the pending SD and re-synthesises against the new template, so a superseded SD is never frozen onto the disk.)

This class is for filesystems being adopted into Peios. A previously-unmanaged filesystem (say, an ext4 volume from a Linux system without KACS) can be mounted with `facs_synthesize_persistent`; every file accessed gets an SD on first access; over time, every file ends up with an SD.

The use case: migrating an existing filesystem to KACS-managed without a single-pass conversion tool. The synthesis-on-access pattern lets the conversion happen incrementally as files are touched. After enough time, every regularly-accessed file has been adopted; rarely-touched files get adopted the next time they are read.

After all files have SDs, the mount can be switched to `facs_deny_missing` for strict mode. The transition is a single `kacs_set_mount_policy` call; existing SDs are unaffected, and any straggler files without SDs become unreachable (which is the desired effect of switching to strict).

## unmanaged

The unmanaged class is the special case for filesystems FACS shouldn't apply to. The kernel uses its own per-operation access rules for files under this mount; no SD-based access check runs at FACS's level.

Specifically:

- `/proc` is `unmanaged`. Access to `/proc/<pid>/*` files is governed by the process-level checks (process SD + PIP, per the two-check rule), not by FACS.
- `/sys` is `unmanaged`. Writes are restricted to `BUILTIN\Administrators` and `SYSTEM` by hardcoded rule.
- `/sys/kernel/security/kacs/*` has explicit SDs set by the kernel; reading these uses the kernel's own logic.

The `unmanaged` class cannot be set via the public ABI. The `kacs_set_mount_policy` syscall rejects attempts to set this class with `-EINVAL`. Only the kernel itself sets this class — at boot, for the pseudo-filesystems it manages.

The reason for restricting this: making a regular filesystem `unmanaged` would mean FACS has no say over it at all, which would be a meaningful operational decision but also a security-relevant one. The kernel reserves the class for its own use.

If you mount a regular filesystem and don't want FACS to apply, the closest you can get is `facs_synthesize_ephemeral` with a permissive mount template — files get a permissive synthesised SD that effectively grants access. This is not the same as no FACS, but it is the closest path available through the public ABI.

## The synthesis chain

For the two synthesising classes, when the kernel needs to produce an SD for a file with none, it uses a chain of sources. The first source that yields a usable SD wins.

```mermaid
flowchart LR
    A["File has no SD"] --> B["Inherit from parent directory"]
    B -->|parent has inheritable ACEs| F["Use computed SD"]
    B -->|parent has none / no parent| C["Mount-level SD template"]
    C -->|template is set| F
    C -->|no template| D["Hardcoded fallback SD"]
    D --> F
```

In order:

1. **Parent directory inheritance.** The kernel reads the parent directory's SD and computes what a newly-created child would inherit (per the [Inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) rules). If this produces a usable SD (with inheritable ACEs from the parent), that SD is used.
2. **Mount-level SD template.** Each FACS-managed mount can carry a default SD template — set via `kacs_set_mount_policy` along with the policy class. The template is a complete self-relative SD (max 64 KB). If the parent did not yield an SD, the template is used.
3. **Hardcoded fallback.** If neither the parent nor the template produces an SD, the kernel uses a hardcoded fallback: `GENERIC_ALL` to SYSTEM and `BUILTIN\Administrators`; `GENERIC_READ | GENERIC_EXECUTE` to Everyone. The owner is set to SYSTEM, group to SYSTEM.

The fallback is conservative: administrators get full control, others get read-and-execute. It is what every filesystem mounted with `facs_synthesize_*` and no specific configuration falls back to, and what every root-of-mount file ends up with on a freshly-installed Peios system.

For most mounts, the fallback is the safety net — typical files have parents with inheritable ACEs, and synthesis lands on step 1. The template is used for files at the root of the mount (where there's no parent on this filesystem) or in unusual cases where parent-inheritance doesn't apply.

## Corrupt SD handling

A universal rule across all FACS-managed classes: **a corrupt SD is treated as a denial**. If the SD on a file exists but fails structural validation — bad header, malformed ACL, invalid SID, exceeds the size limit, anything that prevents the parser from making sense of it — the access check fails with `-EACCES`.

The kernel does not fall back to synthesis when an existing SD is corrupt. The synthesis path is for files with no SD; a file with a *broken* SD is different. The kernel:

1. Detects the corruption when reading the SD.
2. Returns `-EACCES` for the access.
3. Emits an audit event (one per inode per cache population — the same corrupt SD encountered repeatedly during one mount's lifetime produces one audit event for that inode, not one per access).

The corruption audit event is part of the audit stream and useful for diagnostics. A misbehaving backup-restore tool that produced corrupt SDs on a restored set of files generates a flurry of these events when the files are first accessed.

The corrupt-SD rule is the same across all three FACS-managed classes. `facs_deny_missing` denies missing; `facs_synthesize_*` synthesises missing; all three deny corrupt. The "missing" and "corrupt" cases are different — missing is "no SD was attached" and is recoverable through synthesis or operator intervention; corrupt is "the SD that was attached is broken" and requires either fixing the SD or accepting denial.

## Class transitions

Changing a mount's policy class is allowed (subject to access rules covered in [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md)). The interesting transitions:

- **`facs_synthesize_persistent` → `facs_deny_missing`.** Common during migration. As files are adopted by `synthesize_persistent`, they accumulate SDs. Once enough have been adopted, switch to `deny_missing` to enforce strictness. Any file still missing an SD becomes unreachable; an administrator can either accept that or set SDs on the holdouts.
- **`facs_synthesize_ephemeral` → `facs_synthesize_persistent`.** Less common. Would convert an ephemeral mount to a persistent one — the next access of any file without a stored SD would write the synthesised SD back. This effectively "snapshots" the synthesis on first access.
- **`facs_deny_missing` → `facs_synthesize_*`.** Unusual; would relax strictness. The kernel allows it but the operational reasoning is rarely good — synthesis is a recovery mechanism, not an everyday setting.

A class transition is just a policy update; no files are touched at the transition. The new policy applies to future accesses. Existing cached state (synthesised SDs in memory) may need to be re-evaluated — see the generation counter in [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

## Where to go next

For how each filesystem physically stores the SD, read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

For the syscalls that set and read a mount's policy, read [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

For choosing a policy from the command line at attach time, read [mount](/peios/using-peios/mount-policies/mount.md).

---

# SD storage by filesystem

_Peios / Using Peios / Mount policies_

> How each filesystem stores SDs — ext4 xattrs and ea_inode, NTFS round-tripping via system.ntfs_security, FAT's in-memory synthesis — and the implications.

A security descriptor is structurally a single piece of metadata that has to live somewhere. Different filesystems have different ways to hold metadata; some have built-in security-attribute support (NTFS), some have generic extended attribute support that fits (ext4, XFS, Btrfs), some have neither (FAT, exFAT). The mount-policy class you choose for a filesystem depends partly on what the filesystem can store and partly on what behaviour you want.

This page covers the storage mechanism each filesystem type uses and the implications.

## The canonical xattr

For filesystems that support extended attributes, Peios stores the SD in the **`security.peios.sd`** xattr. The name is in the `security.*` namespace, which requires privileged access to read or write at the filesystem layer — which doesn't matter for FACS-managed access (FACS denies direct xattr operations on the SD xattr unconditionally; access goes through `kacs_get_sd` / `kacs_set_sd`), but does mean the xattr survives operations that respect security xattrs.

For NTFS, the xattr name is **`system.ntfs_security`** — the native NTFS security attribute, accessed through the same xattr interface. Using the NTFS-native name means SDs round-trip cleanly between Peios and other operating systems that read NTFS.

The choice of xattr name is filesystem-specific:

| Filesystem | xattr name |
|---|---|
| ext4, XFS, Btrfs, tmpfs, others with generic xattr support | `security.peios.sd` |
| NTFS (via the `ntfs3` driver) | `system.ntfs_security` |

For other filesystems (with no xattr support or no convention), the SD is held in memory and re-synthesised on each cold open. See the per-filesystem sections below.

## ext4

ext4 supports xattrs natively. SDs up to about 4 KB fit in the inline xattr space; larger SDs require the **`ea_inode`** feature.

`ea_inode` is an ext4 feature that lets a single xattr value spill into a dedicated inode rather than being limited to the size of the inode's inline xattr area. With `ea_inode` enabled at filesystem creation time (or enabled later via `tune2fs`), an SD can be up to the standard 65,535-byte SD limit.

`mke2fs` on Peios enables `ea_inode` by default for ext4, and raises the inode size to 512 bytes so that a typical SD fits inline in the first place — see [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md). Real-world SDs rarely exceed the inline xattr space, but pathological cases (deeply-nested ACEs, lots of inherited ACEs from a complex parent) can push past 4 KB; `ea_inode` accommodates them.

If an ext4 filesystem without `ea_inode` is mounted on Peios and a file has an SD larger than its inline xattr space could hold, the filesystem's xattr write would fail. The kernel handles this by either:

- Refusing the `kacs_set_sd` call with an error indicating the filesystem cannot hold the SD.
- Or, in the `facs_synthesize_persistent` case, refusing the write-back and treating the file as if it has no SD (re-synthesising next time).

The cleanest fix is to enable `ea_inode`. For everyday use, the inline xattr space is sufficient.

## XFS

XFS supports xattrs natively, up to 64 KB per attribute by default. This is comfortably more than the SD size limit (65,535 bytes), so SDs fit without any special configuration.

XFS is a fine choice for FACS-managed filesystems. No `ea_inode`-style consideration is needed; SDs just work.

## Btrfs

Btrfs supports xattrs natively. Small xattrs are stored inline; larger ones get their own extent. SDs of any practical size fit without special handling.

Btrfs's snapshot and clone behaviour is interesting for SDs: a Btrfs snapshot of a directory includes the SDs of every file within. A snapshot is a point-in-time view; the SDs in the snapshot reflect what they were when the snapshot was taken. If the original files' SDs are subsequently modified, the snapshot still has the old SDs. This is the expected behaviour but worth noting for backup/snapshot workflows.

## tmpfs and devtmpfs

tmpfs is an in-memory filesystem. It supports xattrs, but everything it stores lives in RAM and is lost on unmount or reboot. SDs on tmpfs are present while the filesystem is mounted; they vanish when it is unmounted.

tmpfs is typically mounted with `facs_synthesize_ephemeral`. The synthesis happens in memory anyway (the tmpfs storage *is* memory), so there's no operational difference between "store an SD in the tmpfs xattr" and "synthesise an SD into the kernel's per-inode cache".

`devtmpfs` is the kernel-managed filesystem for device nodes. SDs on devtmpfs are applied by **udev rules**, not by FACS-driven synthesis. The udev daemon (or its Peios equivalent) sets the SD on each device node as the node is created. This is a different model from the synthesis-based pattern other filesystems use; it works because the device-node population is controlled centrally and the SDs can be set deterministically.

devtmpfs uses the `facs_synthesize_ephemeral` class, but in practice the synthesis path is rarely hit because udev provides the SDs.

## NTFS — round-trip via ntfs3

NTFS is the Windows-native filesystem. Peios mounts it through the **`ntfs3`** kernel driver, which exposes the NTFS security attribute via the standard xattr interface under the name `system.ntfs_security`.

The implications:

- SDs written by Peios to an NTFS volume use the same on-disk format as Windows. A Windows system reading the same volume sees the SD natively.
- SDs written by Windows to an NTFS volume are readable by Peios. Round-tripping a volume between the two operating systems preserves SDs.

This is the "binary-compatible" property the SD format gives. The wire format is the same; the filesystem driver translates between the xattr interface and the on-disk security attribute.

NTFS volumes are typically mounted `facs_synthesize_ephemeral` rather than `facs_deny_missing`. The reasoning: an NTFS volume from Windows may have files without Peios-recognised SDs (the SDs are Windows-native and may use principals that don't exist on the Peios system). Ephemeral synthesis lets such files be accessible without modifying the volume's stored SDs.

## FAT and exFAT

FAT and exFAT have **no xattr support at all**. There is no place to store an SD; the on-disk format simply doesn't have the metadata channel.

FACS handles this by using `facs_synthesize_ephemeral` for FAT/exFAT mounts. Every file gets a synthesised SD in memory; no SD is ever written back. The synthesised SD applies for the file's time in the inode cache; when the file is evicted, the SD is gone and will be re-synthesised on next access.

This means FAT/exFAT files cannot be given persistent KACS-style permissions. The synthesised SD is the same every time (assuming the same inputs — parent SD, mount template), so the access decision is deterministic, but there is no way to customise per-file.

For most FAT use cases this is fine — FAT is typically used for removable media or boot partitions where uniform-permissions semantics is acceptable. The mount-level SD template can be configured to grant whatever access pattern is appropriate for the mount as a whole.

## NFS — synthesise locally, enforce remotely

NFS client mounts are a unique case. The actual files live on a remote server; the local filesystem driver is a network protocol implementation, not a real filesystem.

The mount class is `facs_synthesize_ephemeral`. FACS synthesises a local SD per the synthesis chain (typically yielding a sensible default from the mount template) and uses it for local access control. The local FACS check decides whether to forward the operation to the server.

If the local check passes, the operation goes to the server. The server has its own access control (potentially also Peios with FACS, potentially another OS with different rules); the server's access control decides whether the operation actually proceeds. If the server denies, the operation fails with whatever error the protocol returns (typically `-EACCES` or `-EIO`).

This is "dual authority" — both client and server have a say. The client's denial is local-final; the server's denial is remote-final. There is no single source of truth for the access decision.

The implications were covered in [Special cases](/peios/security-fundamentals/file-access/special-cases.md) under "NFS — dual authority": don't trust local FACS results for security, expect I/O errors from server-side denial, the local synthesised SD is not the server's actual SD.

## /proc and /sys

`/proc` and `/sys` are kernel pseudo-filesystems. Their mount-policy class is `unmanaged` — set by the kernel at boot, not changeable via the public ABI.

`/proc` doesn't have an SD per-file in the FACS sense. Access to `/proc/<pid>/*` is gated by the per-process rules (process SD + PIP, from the two-check rule). The kernel implements these checks directly when serving `/proc` file operations.

`/sys` similarly has its own per-file rules. The `/sys/kernel/security/kacs/*` entries have explicit SDs the kernel maintains; other `/sys` files have hardcoded rules ("writes restricted to `BUILTIN\Administrators` and `SYSTEM`").

The `unmanaged` class is what tells FACS to not interfere with these. The kernel knows what it is doing with its own pseudo-filesystems; FACS stays out of the way.

## Stacked filesystems — overlayfs and StrataFS

A stacking filesystem presents files that physically live somewhere else. overlayfs merges a read-only lower layer with a writable upper one; [StrataFS](/peios/using-peios/disks-and-filesystems/stratafs/overview.md) composes several directories into one view. Neither stores a security descriptor of its own, and neither needs to: the descriptor belongs to the file, and the file is on the layer underneath.

So a stack forwards. When the kernel needs the SD of a file in the merged view, it asks the layer that actually holds it, and what comes back is that layer's **effective** descriptor — which is to say, whatever an access check on the underlying file would have used. If the layer underneath *stores* an SD, the stack sees the stored one. If the layer underneath *synthesises*, the stack sees the synthesised one. Synthesis composes upward through the stack; it is not a private arrangement between the kernel and the bottom layer.

That is what makes the common live-boot arrangement work:

```
overlay          <- the merged root, facs_deny_missing
  upper: tmpfs   <- writable scratch, stores real SDs
  lower: squashfs <- read-only image with no SDs, facs_synthesize_ephemeral
```

The overlay stores nothing and can still be `facs_deny_missing`, because neither layer beneath it ever answers "missing": the tmpfs has stored descriptors, and the squashfs synthesises one for every inode.

Two consequences worth holding onto:

**The stack's own policy class governs only what happens when every layer beneath it has nothing to offer.** Pick it for that case. Over a synthesising lower, `facs_deny_missing` is the strict-and-correct choice; over an `unmanaged` lower it would lock the whole view.

**A copy-up does not carry the lower's descriptor up with it.** When overlayfs copies a file from the lower layer to the upper to make it writable, it creates a genuinely new inode in the upper, and that inode gets its descriptor by ordinary [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) from its parent in the upper — not by duplicating the lower's. This is deliberate: `security.peios.sd` is not userspace-writable (SD mutation is `kacs_set_sd`'s job), so a verbatim copy could not be performed even in principle, and the inherited answer is the right one anyway. Every other extended attribute *is* copied up normally.

## Summary

| Filesystem | SD storage | Typical mount class |
|---|---|---|
| ext4 | `security.peios.sd` xattr; `ea_inode` for SDs > 4 KB | `facs_deny_missing` (for system mounts) |
| XFS | `security.peios.sd` xattr, native large support | `facs_deny_missing` |
| Btrfs | `security.peios.sd` xattr, native | `facs_deny_missing` |
| tmpfs | `security.peios.sd` xattr in memory | `facs_synthesize_ephemeral` |
| devtmpfs | xattr in memory, populated by udev | `facs_synthesize_ephemeral` |
| NTFS (ntfs3) | `system.ntfs_security` xattr, NTFS-native | `facs_synthesize_ephemeral` (typical) |
| FAT / exFAT | No SD storage; in-memory only | `facs_synthesize_ephemeral` |
| NFS client | No client-side storage; synthesise locally | `facs_synthesize_ephemeral` |
| squashfs | No SD storage unless the image was built with one | `facs_synthesize_ephemeral` |
| overlayfs | None of its own — reads the real layer | `facs_deny_missing` |
| StrataFS | None of its own — reads the provider | `facs_deny_missing`, not settable |
| /proc | n/a — no FACS | `unmanaged` |
| /sys | n/a — kernel-managed SDs | `unmanaged` |

The pattern: real on-disk filesystems with xattr support get one of the `facs_*` classes depending on policy needs. Pseudo-filesystems and the kernel's own filesystems are `unmanaged`. Filesystems without xattr support default to ephemeral synthesis as the only viable mode. Stacking filesystems store nothing and defer to the layer beneath.

## Where to go next

For what each policy class does with a missing SD, read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

For setting and reading a mount's policy at runtime, read [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md).

For the structure of the SD being stored, read [Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md).

---

# Managing mounts

_Peios / Using Peios / Mount policies_

> kacs_set_mount_policy and kacs_get_mount_policy, the generation counter behind lazy invalidation, and the SeTcbPrivilege requirement.

A mount's policy is set via a syscall, read via a syscall, and otherwise sits as kernel-internal state on the filesystem's superblock. Mount-policy changes are administrative — they require `SeTcbPrivilege` — and they take effect lazily, without the kernel walking the filesystem to update anything.

This page covers the two syscalls (`kacs_set_mount_policy`, `kacs_get_mount_policy`), the generation counter that makes lazy invalidation work, and what does and does not happen at a policy change.

## kacs_set_mount_policy

The write syscall:

```
result = kacs_set_mount_policy(fd, args)
```

Where `fd` is a file descriptor referring to any object on the target superblock (the kernel uses the fd to identify which superblock — the file itself does not need to be the root of the mount). `args` is a `kacs_mount_policy_args` struct:

| Field | Meaning |
|---|---|
| `policy` | The new policy class (one of `KACS_MOUNT_POLICY_DENY_MISSING`, `KACS_MOUNT_POLICY_SYNTHESIZE_EPHEMERAL`, `KACS_MOUNT_POLICY_SYNTHESIZE_PERSISTENT`). |
| `flags` | Reserved; must be zero. |
| `generation` | (Output) The new generation counter value after the change. |
| `template_sd_ptr`, `template_sd_len` | Optional mount-level SD template. Max 64 KiB. |

The kernel:

1. Validates the fd and identifies the superblock.
2. Validates the policy value. Attempts to set `KACS_MOUNT_POLICY_UNMANAGED` (which is not settable via this ABI) are rejected with `-EINVAL`.
3. Validates the template SD if provided — must parse, must be within size limits.
4. Checks the caller's privileges. `SeTcbPrivilege` is required.
5. Atomically updates the superblock's policy class, the template SD, and increments the generation counter.
6. Writes the new generation to `args.generation`.
7. Returns 0.

The change is **atomic at the superblock level** — every future access against any inode on this superblock sees the new policy. There is no transitional state where some inodes are under the old policy and others under the new.

### Privilege requirement

> [!IMPORTANT]
> `SeTcbPrivilege` is the privilege gating this call. The privilege is held by peinit and a handful of other TCB components; not by ordinary services or administrators. In practice this means mount policy is configured at boot (by peinit, applying mount-time configuration) or by a privileged management daemon. Ad-hoc policy changes are rare.

The reasoning: changing a mount's policy is an administrative decision that affects the entire filesystem's access semantics. It belongs in the TCB tier, not in the regular-administrator tier. An ordinary administrator who wants to change a mount's policy goes through a tool that itself has the privilege.

### The template SD

The optional template SD is the default SD used during synthesis when the parent's inheritance does not yield one. The kernel stores the template on the superblock; subsequent synthesis-on-missing calls use it.

The template is a complete self-relative SD — owner, primary group, DACL, optional SACL. The kernel validates the template at policy-set time:

- Must be in self-relative format.
- Must include an owner (an SD without one is malformed and rejected).
- Must fit within 65,535 bytes total; the template specifically is capped at 64 KiB.
- ACLs and ACEs must parse.

A bad template at policy-set time is rejected with `-EINVAL`; the current policy and template are unchanged.

The template can be omitted entirely (zero pointers) for mounts that don't need one. The synthesis chain then falls through to the hardcoded fallback for any file whose parent doesn't yield an SD.

## kacs_get_mount_policy

The read syscall:

```
result = kacs_get_mount_policy(fd, args)
```

Same `fd` and `args` semantics, returning the current policy class, current template (if buffer provided), and current generation counter.

The kernel:

1. Validates the fd, identifies the superblock.
2. Checks the caller's privileges. `SeTcbPrivilege` is required to read.
3. Writes the policy class, generation, and template to `args` (template only if a buffer was provided).
4. Returns 0.

Like the write syscall, this is `SeTcbPrivilege`-gated. The mount policy is system-level state, exposed only to TCB-tier callers.

This is the kernel's read-back-the-current-policy interface. A management tool reads the policy, perhaps applies a transformation, and writes the new value back. The read-modify-write pattern is what most policy management code uses.

## The generation counter

A subtle but important feature: the kernel maintains a **monotonic generation counter** per superblock. Each successful `kacs_set_mount_policy` (or template change) increments it.

The purpose is **lazy invalidation**.

Without the counter, a policy change would either need to immediately propagate to every cached SD on the filesystem (expensive — could be millions of inodes) or accept that cached SDs from before the change remain valid (incorrect — the new policy might say different things).

With the counter, the kernel can do something smarter:

1. Each in-kernel cached SD or synthesised entry records the generation it was derived from.
2. At access time, the kernel compares the cached entry's generation against the superblock's current generation.
3. If they match, the cached entry is current; use it.
4. If they differ, the cached entry is stale; discard it and re-derive.

The kernel does **not** walk the filesystem at the policy change. The walk happens implicitly, one inode at a time, as accesses arrive. An access that arrives 30 seconds after the policy change is the first to invalidate that specific inode's cached state; the access pays a small cost to re-derive, but every subsequent access uses the new cached state.

This is why mount policy changes are cheap to apply. The expensive work (walking inodes) never happens; what happens instead is per-inode re-derivation on first access after the change.

### What the generation counter affects

The counter affects in-kernel **cached** state derived from the mount policy:

- **Missing-SD synthesised entries.** If a file had a missing SD and was synthesised on-the-fly, the synthesised SD is cached. A policy change invalidates this cache; the next access re-synthesises.
- **Ephemeral-synthesis caches for `facs_synthesize_ephemeral` mounts.** Same.
- **Template-derived information** — the kernel's representation of the mount template is regenerated on next access.

The counter does **not** affect:

- **On-disk SDs.** A file that has a stored SD continues to have that SD. The policy change doesn't rewrite stored SDs.
- **Already-open file descriptors.** The cached granted mask on an fd is independent of the mount-policy generation; it's the result of the access check at open time. Policy changes after open don't affect open fds. (This is the standard check-at-open model.)
- **Inodes not currently in cache.** They have nothing to invalidate; the next access just goes through the normal path with the current policy.

### Reading the counter

The generation counter is exposed in the output of `kacs_get_mount_policy`. A management tool can read the counter to know when policy was last changed, or compare against a previously-read value to detect external changes.

The counter is per-superblock; different filesystems have unrelated counter values. The counter starts at some implementation-defined value at mount time and increases monotonically with each policy change.

## What happens at a policy change

To summarise, when `kacs_set_mount_policy` succeeds:

1. The superblock's policy class field is updated.
2. The superblock's template SD is updated.
3. The superblock's generation counter is incremented.
4. Open file descriptors against files on this mount are unaffected (cached granted masks intact).
5. Cached in-memory state derived from the old generation will be invalidated on first access after the change.
6. Files with stored SDs continue to have those SDs.
7. The filesystem itself is not walked, not modified, not reorganised.

The change is essentially a small superblock update plus a lazy invalidation marker. The work happens at future accesses, distributed across actual usage.

## What policy changes do not do

A few clarifications:

- **They don't rewrite SDs on disk.** A change from `facs_synthesize_ephemeral` to `facs_synthesize_persistent` doesn't go back and write synthesised SDs to disk. Files that were synthesised under the old policy continue to have no on-disk SD; the next access re-synthesises (now writes back, per the new persistent policy).
- **They don't close open files.** Processes with handles to files on the mount continue to have those handles. The cached granted masks are unchanged.
- **They don't propagate across mounts.** A policy change on one mount doesn't affect any other mount, even if they reference the same underlying device (per the per-superblock rule).
- **They don't change the FACS handle model.** The new policy applies to future access checks; existing handles use their cached masks per usual.

## Use patterns

A handful of common patterns for managing mount policy:

**Boot-time policy.** peinit applies the policy class and template for each mounted filesystem at boot. The configuration source is typically registry-based; peinit reads the configuration and calls `kacs_set_mount_policy` for each mount.

**Migration from a non-Peios filesystem.** Mount with `facs_synthesize_persistent` initially. Let usage gradually adopt files into having stored SDs. Once enough have been adopted, switch to `facs_deny_missing` (or leave it as `synthesize_persistent` if you want the synthesis-on-missing safety net).

**Removable media insertion.** When a removable filesystem is mounted (a USB drive, an optical disc), the mount setup chooses `facs_synthesize_ephemeral`. The volume's contents become reachable without modifying the on-disk metadata; ejection cleanly leaves the media untouched.

**Policy uplift on a hardened deployment.** A deployment starting with `facs_synthesize_ephemeral` everywhere (permissive) can migrate to `facs_synthesize_persistent` (filling in stored SDs) and then to `facs_deny_missing` (strict mode). Each transition is one `kacs_set_mount_policy` call per mount.

## Errors

Possible failures from `kacs_set_mount_policy`:

| Error | Cause |
|---|---|
| `-EBADF` | The fd is invalid. |
| `-EPERM` | The caller does not hold `SeTcbPrivilege`. |
| `-EINVAL` | Setting an `unmanaged` policy via the public ABI; setting an unknown policy value; non-zero reserved flags; malformed template SD; template size exceeded. |

Possible failures from `kacs_get_mount_policy`:

| Error | Cause |
|---|---|
| `-EBADF` | The fd is invalid. |
| `-EPERM` | The caller does not hold `SeTcbPrivilege`. |
| `-ERANGE` | A template buffer was provided but is smaller than the template. The required size is written to the output. |

In normal operation both calls succeed. Failures are typically privilege issues or malformed inputs.

## See also

- [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) — what each settable class does.
- [mount](/peios/using-peios/mount-policies/mount.md) — setting the policy from the command line at attach time.
- [Privileges](/peios/security-fundamentals/privileges/overview.md) — the privilege model behind the SeTcbPrivilege gate.

---

# mount

_Peios / Using Peios / Mount policies_

> Attach a filesystem to the Peios mount tree — new mounts, binds, moves, remounts and propagation changes — and apply a KACS mount policy at mount time.

`mount` attaches a filesystem to the Peios mount tree. With no operands it instead *lists* what is currently mounted. It is a faithful reworking of the util-linux `mount(8)` surface for Peios, with two structural differences: Peios has **no `/etc/fstab` and no `/etc/mtab`**, so every mount is described entirely on the command line and live mount state is read from the kernel; and a `mount` can apply a **KACS mount policy** to the new filesystem at attach time (see [Mount policies](/peios/using-peios/mount-policies/overview.md)).

```
mount [-t TYPE] [-o OPTIONS] SOURCE TARGET
mount [-o remount,OPTIONS] TARGET
mount --bind|--rbind|--move SOURCE TARGET
mount --make-shared|--make-private|... TARGET
mount [-l] [-t TYPE]
```

Internally `mount` uses the fd-based mount API (`fsopen` / `fsconfig` / `fsmount` / `move_mount`, plus `open_tree` and `mount_setattr`), never the classic single-shot `mount(2)`. This matters in one place you can see: when a mount fails, the kernel's `fs_context` message log is drained and printed as the reason, even without `-v`.

## Operands and argument shapes

Because there is no fstab to consult, operand resolution is strict:

- **No operands, no verb** — list mode (see [below](#list-mode)).
- **Two operands** — `SOURCE TARGET`.
- **One operand** — an error. In util-linux a lone operand is resolved through fstab; Peios has none, so a single operand cannot be turned into a `SOURCE TARGET` pair. Supply both, or use `--source` / `--target`.
- **A lone target** is valid only for `-o remount` and for a standalone propagation change (`--make-*`), which act on an existing mount point.

`--source SRC` and `--target DIR` name the operands explicitly and may be combined with a single positional. `--target-prefix DIR` prepends `DIR/` to the target after it is chosen. Options may be interspersed with operands (`mount SRC -o ro TGT`), matching util-linux.

Paths, `-o key=value` values, labels and UUIDs are handled as **opaque byte strings**, never assumed to be UTF-8; an embedded NUL is a usage error.

### Canonicalisation

By default source and target paths are canonicalised (made absolute, symlinks resolved). `-c` / `--no-canonicalize` disables that; `X-mount.nocanonicalize[=source|target]` is the `-o` form and can disable it for just one side. The final path handed to the kernel is protected against a TOCTOU symlink swap on its last component.

## Operation modes (verbs)

`mount` performs one of several operations. The **structural verbs** — bind, rbind, move, beneath and remount — are mutually exclusive with one another. A **propagation** change is *not* a structural verb: it may stand alone on a target, or trail another verb or a new mount in the same command (applied afterwards as one or more `mount_setattr` steps), and several may be combined.

| Verb | How to request it | What it does |
|---|---|---|
| **New mount** | `mount [-t T] SRC TGT` | Attach a fresh instance of the filesystem at `SRC` onto `TGT`. |
| **Bind** | `-B` / `--bind`, or `-o bind` | Make an existing subtree visible at a second location. |
| **Recursive bind** | `-R` / `--rbind`, or `-o rbind` | Bind a subtree together with every mount underneath it. |
| **Move** | `-M` / `--move`, or `-o move` | Relocate an existing mount to a new mount point. |
| **Move beneath** | `--beneath SRC TGT` | Attach `SRC` *beneath* the mount currently at `TGT` (the kernel enforces several constraints; violations surface as exit 32). |
| **Remount** | `-o remount[,...] TGT` | Change the options of an existing mount (see [Remounting](#remounting)). |
| **Propagation** | `--make-*` / `--make-r*`, or the `-o` tokens below | Change how mount/unmount events propagate across a subtree. |
| **List** | `mount` / `mount -l` | Print the current mounts. |

### Propagation flags

Each of these sets the propagation type of the mount at the target. The `--make-r*` (and `r`-prefixed `-o`) forms apply recursively to the whole subtree; the recursive form is all-or-nothing (it either applies to the entire subtree or to none of it).

| Flag | `-o` token | Recursive flag | Recursive `-o` token | Meaning |
|---|---|---|---|---|
| `--make-shared` | `shared` | `--make-rshared` | `rshared` | Events propagate to and from peer mounts. |
| `--make-slave` | `slave` | `--make-rslave` | `rslave` | Events propagate *in* from the master but not back out. |
| `--make-private` | `private` | `--make-rprivate` | `rprivate` | No propagation either way. |
| `--make-unbindable` | `unbindable` | `--make-runbindable` | `runbindable` | Private, and cannot be bind-mounted. |

A freshly created mount, bind or move is **private** by default unless its destination's parent is shared, in which case it joins that peer group — the standard kernel rule.

## Source specification

| Form | Resolution |
|---|---|
| Device path (`/dev/sda1`) | Used directly. |
| `-L LABEL` / `LABEL=`, `-U UUID` / `UUID=`, `PARTLABEL=`, `PARTUUID=` | Resolved to a device via libblkid. No match is a mount failure (exit 32). |
| A directory or regular file | Used directly (a bind source or target; a file may be a bind target). |
| A pseudo source (`tmpfs`, `proc`, `sysfs`, `none`, …) | Passed through; `-t` is required because it cannot be probed. |
| An image file | Attached through a loop device (see [Loop devices](#loop-devices)). |

## Filesystem type

`-t TYPE` names the type explicitly. `-t auto`, or omitting `-t` entirely, asks libblkid to **safely probe** the source; an ambiguous or multiply-signed device is refused (exit 32) rather than guessed at. `X-mount.auto-fstypes=LIST` constrains the probe candidates. Type *lists* and `no<type>` negation are not accepted in the mounting path (they remain meaningful only as a [listing filter](#list-mode)).

## The `-o` option language

`-o` takes a comma-separated list of `key` or `key=value` tokens and is repeatable (all occurrences are joined). A `key="..."` double-quoted value protects embedded commas; the `key=value` split is on the **first** `=`. Every token is sorted into one of the following categories.

### Per-mount attributes

Applied to the mount itself. Each accepts its negation; `ro`/`rw` additionally accept a `=vfs` / `=fs` / `=recursive` scope qualifier (bare is `=vfs`, non-recursive; `=fs` targets the superblock read-only flag; `=recursive` applies to the whole subtree).

| Option | Effect |
|---|---|
| `ro` / `rw` | Read-only / read-write. |
| `suid` / `nosuid` | Honour / ignore set-user-ID bits. |
| `dev` / `nodev` | Allow / disallow device nodes. |
| `exec` / `noexec` | Allow / disallow execution. |
| `atime` / `noatime` | Update / never update access times. |
| `relatime` / `norelatime` | Relative-atime updates on or off. |
| `strictatime` / `nostrictatime` | Strict-atime updates on or off. |
| `diratime` / `nodiratime` | Directory access-time updates on or off. |
| `nosymfollow` | Do not follow symlinks on this mount. There is no positive `symfollow` token — the attribute is cleared only via a remount mask. |

The atime tokens share a single mode field; the last one specified wins.

### Superblock flags

| Option | Effect |
|---|---|
| `sync` / `async` | Synchronous / asynchronous writes. |
| `dirsync` | Synchronous directory updates. |
| `lazytime` / `nolazytime` | Lazy on-disk timestamp updates on or off. |
| `iversion` / `noiversion` | Inode version counting on or off. |
| `silent` / `loud` | Suppress or emit certain kernel messages. |
| `mand` / `nomand` | Obsolete (removed from the kernel). Accepted and ignored with a note under `-v`. |

### Filesystem-specific parameters

Any token not recognised above is forwarded verbatim to the filesystem: `key=value` as a string parameter, a bare `key` as a flag. There is no built-in allow-list and no length limit; a rejection by the filesystem is reported with the offending key and the kernel's message.

For example, StrataFS takes its precedence-ordered directory stack through
`strata=`:

```sh
mount -t stratafs none /bin -o 'strata=/lcl/bin+create:/usr/bin+ro'
```

See [StrataFS](/peios/using-peios/disks-and-filesystems/stratafs/overview.md) for the stack flags, merge
and write-routing model, and the `stratafs` inspection command.

### Userspace-only tokens

These never reach the filesystem. They include the meta-verbs `remount`, `bind`, `rbind`, `move`; the loop controls `loop` / `loop=/dev/loopN`, `offset=`, `sizelimit=` (numeric values accept `K`/`M`/`G`/`T` and `KiB`/`MiB`/… suffixes); the propagation tokens listed [above](#propagation-flags); and `defaults`, which expands to `rw,suid,dev,exec,async` (later tokens override it).

### Functional `X-mount.*` options

| Option | Effect |
|---|---|
| `X-mount.mkdir[=mode]` | Create the target directory if missing (default mode `0755`; the alias of `-m`). |
| `X-mount.subdir=DIR` | Attach subdirectory `DIR` of a freshly mounted filesystem at the target. Effective only for a new-instance mount; silently ignored (noted under `-v`) for bind/move/remount/propagation. |
| `X-mount.noloop` | Suppress the implicit loop device for a regular-file source. |
| `X-mount.auto-fstypes=LIST` | Constrain the `-t auto` probe to these types. |
| `X-mount.nocanonicalize[=source\|target]` | The `-o` form of `-c`; with `=source` or `=target` it disables canonicalisation for just that path. |

`X-mount.idmap` and `X-mount.owner` / `group` / `mode` are **not supported** and are rejected with a usage error: Peios ownership is SID/security-descriptor based, so the fix is to add a SID/ACE to the descriptor rather than remap or chown.

### KACS mount policy

This is the genuinely Peios-specific part of `mount`. A mount policy is a per-superblock setting that governs how FACS treats the filesystem; the full model is described in [Mount policies](/peios/using-peios/mount-policies/overview.md) and its detail pages. `mount` can set that policy at attach time:

| Option | Policy class |
|---|---|
| `-o policy=deny-missing` | `facs_deny_missing` — a file with no SD is unreachable. |
| `-o policy=synth-ephemeral` | `facs_synthesize_ephemeral` — a missing SD is synthesised in memory only. |
| `-o policy=synth-persist` | `facs_synthesize_persistent` — a missing SD is synthesised and written back. |
| `--synth-sddl SDDL` | Provide the mount-level SD template used during synthesis (only valid with a `synth-*` policy). |

`policy=unmanaged` is **not** user-settable; only the kernel sets the unmanaged class, for its own pseudo-filesystems. `policy=` is valid only on a **new mount** of a real filesystem — combining it with bind/move/remount/propagation, or with list mode, is a usage error. See [Policy classes](/peios/using-peios/mount-policies/policy-classes.md) for what each class does and [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) for how the SD is physically stored.

The policy is applied to the *detached* filesystem before it is attached: if setting it fails, nothing is ever published with an unintended policy (no rollback needed). Because setting a mount policy is a `SeTcbPrivilege`-gated operation (see [Managing mounts](/peios/using-peios/mount-policies/managing-mounts.md)), a caller without that privilege gets a clean `EPERM` (exit 1) and no mount. `--synth-sddl` is validated client-side first — it must be well-formed SDDL and must include an owner.

Peios applies no coarse `uid==0` check anywhere: the `mount` applet is not installed set-user-ID, and every privileged action is authorised per-operation by KACS.

### Remounting

`-o remount` changes the options of an existing mount without detaching it. Peios does **not** perform the util-linux "read the old flags and re-supply them" dance — the fd-based API applies deltas rather than resetting, so each remount touches only what you name. Per-mount attributes (`ro`/`rw`, `nosuid`, atime, …) are changed via `mount_setattr`; superblock flags, filesystem parameters and `ro=fs`/`rw=fs` go through the superblock reconfigure path. `=recursive` remounts the whole subtree, all-or-nothing.

A **bind** mount shares its source's superblock, so any superblock-level option on a bind remount (a Category-B flag, a filesystem parameter, or `ro=fs`/`rw=fs`) is refused as a usage error rather than silently mutating the shared superblock for every mount of that filesystem. Only per-mount VFS attributes are valid on a bind remount.

## Loop devices

A regular-file source is backed by a loop device automatically **when the type is unspecified or the filesystem is recognised by libblkid**; `X-mount.noloop` suppresses this. `-o loop` forces auto-allocation of a free device, `loop=/dev/loopN` names one, and `offset=` / `sizelimit=` select a region of the file (they imply loop and are an error against a real block device). A backing file already attached at the same offset and size is *reused* rather than doubly attached, to avoid corruption. Loops created by `mount` are auto-cleared by the kernel on unmount, so they do not leak; `umount -d` force-clears the rest (see [`umount`](/peios/using-peios/mount-policies/umount.md)).

## Other flags

| Flag | Effect |
|---|---|
| `-t`, `--types TYPE` | Filesystem type, or `auto`. |
| `-o`, `--options LIST` | Mount options (above); repeatable. |
| `-r`, `--ro`, `--read-only` | Mount read-only (`-o ro`). |
| `-w`, `--rw`, `--read-write` | Mount read-write and **forbid** the automatic read-only fallback on a write-protected device (`-o rw`). |
| `--source SRC` / `--target DIR` | Name the operands explicitly. |
| `--target-prefix DIR` | Prepend `DIR/` to the target. |
| `-B`, `--bind` / `-R`, `--rbind` / `-M`, `--move` / `--beneath` | The structural verbs. |
| `--make-*`, `--make-r*` | Propagation changes. |
| `--exclusive` | Force a unique superblock instance (no reuse). Meaningful only for multi-instance filesystems (e.g. `tmpfs`) or read-only block mounts; on an already-mounted writable block device it fails with `EBUSY` (exit 32). |
| `-m`, `--mkdir[=MODE]` | Create the target directory if missing (default mode `0755`). |
| `-L`, `--label LABEL` / `-U`, `--uuid UUID` | Select the source by filesystem label / UUID. |
| `-c`, `--no-canonicalize` | Do not canonicalise paths. |
| `-f`, `--fake` | Dry run: parse, resolve and plan everything but skip the mount syscalls and the policy step. |
| `-v`, `--verbose` | Narrate resolved values and each syscall; drain the kernel `fs_context` log. Repeatable but `-vv` is the same as `-v`. |
| `-l`, `--show-labels` | In list mode, append each filesystem's label. |
| `--onlyonce` | Skip the mount if it is already present (a driver-aware check against live mount state, not a naive string match). |
| `-N`, `--namespace NS` | Operate inside mount namespace `NS` (a PID, an ns file path, or a named namespace). Source resolution happens in the caller's namespace; the mount lands in the target namespace. |
| `-i`, `--internal-only` | Do not invoke a `mount.<type>` helper. |
| `-n`, `--no-mtab` | Accepted and ignored (Peios has no mtab). |
| `--synth-sddl SDDL` | KACS synth-policy template SD (above). |
| `-h`, `--help` / `-V`, `--version` | Standard. |

No external mount helpers ship in this version, so a network or FUSE type fails with "no helper for type" (exit 1) — a clean deferral, not a crash.

## List mode

With no operands, `mount` prints one line per mount, read from `/proc/self/mountinfo`:

```
SOURCE on TARGET type FSTYPE (OPTIONS)
```

`mount -t TYPE` with no operands *filters* the list by filesystem type instead of mounting; here type lists (`-t ext4,xfs`) and `no<type>` negation (`-t nosysfs`) are honoured. `-l` appends ` [LABEL]` to each line when a label is known.

Details of the rendering: the option field is the positional VFS-then-superblock merge with a coalesced `ro`/`rw`, not sorted; mountinfo octal escapes are decoded; control characters in the mount point become `?`; and the source is shown as the kernel recorded it, with `/dev/loopN` resolved to its backing file and `/dev/dm-N` to `/dev/mapper/<name>`. The listing shows only mounts the caller's token may observe; a partial or empty view is correct, not an error, and the paths of filtered-out parents are never reconstructed.

For a device-oriented view of what is available to mount, use [`lsblk`](/peios/using-peios/mount-policies/lsblk.md).

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Incorrect invocation or a permission denial — bad flags, mutually-exclusive verbs, a lone operand, an invalid `policy=` or `--synth-sddl`, an authorisation failure (`EPERM`/`EACCES`), an embedded NUL, or "no helper for type". |
| `2` | System error (out of memory, no free loop device, cannot fork). |
| `4` | Internal error (an invariant failure). |
| `8` | Interrupted (SIGINT), after signal-safe cleanup. |
| `32` | Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a `--beneath`/move/`--exclusive` kernel refusal. |
| `126` | An external `mount.<type>` helper was found but failed to execute (moot until a helper ships). |

Code `16` (mtab) is never produced. Code `64` (some succeeded, some failed) only arises when several source arguments are given and at least one succeeds while another fails.

---

# umount

_Peios / Using Peios / Mount policies_

> Detach a filesystem from the Peios mount tree by mount point or by source, with lazy, forced, recursive and all-targets variants.

`umount` detaches a filesystem from the Peios mount tree. It is the counterpart of [`mount`](/peios/using-peios/mount-policies/mount.md) and, like it, reads live mount state from the kernel (`/proc/self/mountinfo`) rather than any `/etc/mtab` — Peios has none. Each operand is resolved against that live state and unmounted with `umount2(2)`.

```
umount [-lfRA] [-dr] TARGET|SOURCE...
```

## Operands and how they are resolved

Each operand is either a **mount point** or a **source**:

- If the (canonicalised) operand is itself a mount point in the current mount table, it is unmounted directly. This is always unambiguous.
- Otherwise the operand is treated as a **source** and matched against the sources in the mount table. If it is mounted in exactly one place, that place is unmounted. If it is mounted in **several** places, `umount` refuses with an error naming the candidates — resolve it by naming a specific mount point, or use `-A` to unmount all of them.

If an operand matches nothing, it is "not mounted": normally an error (exit 32), but `-g` makes that a success and `-q` suppresses the message.

Several operands may be given in one invocation, and options may be interspersed with them (`umount /a -f /b`). Paths are handled as opaque bytes; an embedded NUL is a usage error. By default each operand is canonicalised; `-c` disables that and additionally selects `UMOUNT_NOFOLLOW` so the kernel does not follow a symlink in the final component.

## Recursive and all-targets unmounts

Two flags expand a single operand into multiple unmounts. Within one operand the expansion **stops on the first failure**.

- **`-R` / `--recursive`** unmounts the target and everything mounted underneath it, including any over-mount stack at a single point, ordered deepest-first.
- **`-A` / `--all-targets`** unmounts *every* mount point of the given source in the current mount namespace. This is the live-mountinfo "unmount everywhere" complement to source resolution; it is not an fstab feature.
- **`-A -R` together** compose: for each mount point of the source, recurse underneath it.

`-R` and `-r` are mutually exclusive (combining them is a usage error, exit 1).

## Flags

| Flag | Effect |
|---|---|
| `-l`, `--lazy` | Detach the filesystem now and clean up references later (`MNT_DETACH`). |
| `-f`, `--force` | Force the unmount (`MNT_FORCE`), e.g. for an unreachable server. |
| `-R`, `--recursive` | Unmount the target and everything under it (see above). |
| `-A`, `--all-targets` | Unmount every mount point of the given source (see above). |
| `-d`, `--detach-loop` | After unmounting, free the backing loop device. Best-effort and verified: it clears the device only if it still backs the mount just removed, so a recycled loop number is never clobbered. Usually redundant, since `mount`-created loops auto-clear on unmount. |
| `-r`, `--read-only` | If the unmount fails, remount the filesystem read-only instead (via `mount_setattr`). Mutually exclusive with `-R`. |
| `-g`, `--graceful` | Exit `0` when the target is absent or not mounted (rather than failing). Applied unconditionally. |
| `-q`, `--quiet` | Suppress "not mounted" messages. |
| `-c`, `--no-canonicalize` | Do not canonicalise paths; selects `UMOUNT_NOFOLLOW`. Applied unconditionally. |
| `-v`, `--verbose` | Say what is being unmounted. Repeatable. |
| `-N`, `--namespace NS` | Enter mount namespace `NS` (a PID, an ns file path, or a named namespace) before reading its mount table and unmounting. All work happens inside that namespace. |
| `-n`, `--no-mtab` | Accepted and ignored (no mtab on Peios). |
| `-i`, `--internal-only` | Do not invoke a `umount.<type>` helper (none ship). |
| `--fake` | Dry run: resolve operands and report, but skip the unmount syscalls. |
| `-h`, `--help` / `-V`, `--version` | Standard. |

The `umount2` flag mapping is direct: `-l` → `MNT_DETACH`, `-f` → `MNT_FORCE`, `-c` (or a symlinked final component) → `UMOUNT_NOFOLLOW`. `MNT_EXPIRE` is deliberately not exposed, matching util-linux.

### On privilege

As with `mount`, there is no coarse `uid==0` check: the applet is not set-user-ID and every unmount is authorised per-operation by KACS. Where util-linux silently suppresses an option for non-root users (`-c`, and `-g`'s effectiveness), Peios applies it unconditionally.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success (or a `-g` no-op on an absent target). |
| `1` | Incorrect invocation or a permission denial — including `-R` with `-r`, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure. |
| `2` | System error (e.g. cannot read the mount table). |
| `8` | Interrupted (SIGINT). |
| `32` | Unmount failed, or the target is not mounted (unless `-g`). |
| `64` | Several source/target arguments were given and at least one succeeded while another failed. |
| `126` | An external `umount.<type>` helper was found but failed to execute (moot until a helper ships). |

A single `-R` / `-A` / `-A -R` invocation stops on the first failure and yields `32`; the aggregate `64` only appears across multiple top-level arguments. To see what is currently mounted before unmounting, use [`mount`](/peios/using-peios/mount-policies/mount.md) with no arguments or [`lsblk`](/peios/using-peios/mount-policies/lsblk.md).

---

# lsblk

_Peios / Using Peios / Mount policies_

> List block devices as a tree, flat list, JSON or key=value pairs — with filesystem identity, topology, and SD-derived owner and mode columns.

`lsblk` lists the block devices on the system and their relationships — disks, their partitions, and the device-mapper and loop devices layered on top. It is the device-oriented companion to [`mount`](/peios/using-peios/mount-policies/mount.md): where `mount` with no arguments shows what is *mounted*, `lsblk` shows what is *available to mount* and what filesystem sits on each device.

```
lsblk [options] [DEVICE...]
```

By default it prints a tree, one row per device with children indented beneath their parent. Given one or more `DEVICE` operands (a name like `sda`, a full path like `/dev/sda`, or anything resolving to a device node), it re-roots the output at those devices.

## Where the data comes from

Each column is sourced from one of four places, and any that cannot be read degrades to an empty cell rather than aborting the run:

- **sysfs** (`/sys/block`) — the device tree, sizes, and the topology/hardware columns. Peios leaves `/sys/block` unpatched, so this is the standard no-udev path.
- **libblkid** — filesystem identity: `FSTYPE`, `FSVER`, `UUID`, `LABEL`, and the partition-table columns. libblkid is opened at runtime.
- **The device node's security descriptor** — `OWNER` and `MODE`, read the same way [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) reads them.
- **The `/dev/disk/by-*` symlink farm** — `ID-LINK`. Populated by the device manager once it has run; there is deliberately no `/run/udev/data` parser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.

## Output modes

The mode selects how the rows are formatted; it does not change which columns are shown.

| Flag | Mode |
|---|---|
| *(default)* | Indented tree. |
| `-l`, `--list` | Flat list — the same columns, no tree glyphs. |
| `-J`, `--json` | JSON. Flag columns render as bare booleans and `MOUNTPOINTS` as an array; children nest under a `children` key. |
| `-P`, `--pairs` | `KEY="value"` pairs, one device per line. |
| `-r`, `--raw` | Raw, space-separated. Values that could contain a space or control character are hex-escaped (`\xNN`) so fields stay parseable. |
| `-T`, `--tree[=COLUMN]` | Force tree output even alongside `-l`, optionally attaching the tree glyphs to `COLUMN` instead of `NAME`. |

## Columns

With no column flag, `lsblk` prints the default set: `NAME`, `MAJ:MIN`, `RM`, `SIZE`, `RO`, `TYPE`, `MOUNTPOINTS`. Three flags swap in a preset, and `-o` names an explicit list (which wins over all of them):

| Flag | Column set |
|---|---|
| `-o`, `--output LIST` | Exactly the comma-separated columns named (case-insensitive; an unknown name is a usage error). |
| `-O`, `--output-all` | Every available column. |
| `-f`, `--fs` | Filesystem view: `NAME`, `FSTYPE`, `FSVER`, `LABEL`, `UUID`, `MOUNTPOINTS`. |
| `-m`, `--perms` | Permissions view: `NAME`, `SIZE`, `OWNER`, `MODE`. |

The available columns are `NAME`, `KNAME`, `PATH`, `MAJ:MIN`, `FSTYPE`, `FSVER`, `LABEL`, `UUID`, `PTUUID`, `PTTYPE`, `PARTTYPE`, `PARTLABEL`, `PARTUUID`, `MOUNTPOINT`, `MOUNTPOINTS`, `SIZE`, `RO`, `RM`, `HOTPLUG`, `TYPE`, `OWNER`, `MODE`, `MODEL`, `VENDOR`, `REV`, `SERIAL`, `TRAN`, `HCTL`, `ALIGNMENT`, `MIN-IO`, `OPT-IO`, `PHY-SEC`, `LOG-SEC`, `STATE`, `ROTA`, `SCHED`, `PKNAME`, and `ID-LINK`.

### The `OWNER` and `MODE` columns

`OWNER` and `MODE` describe the *device node*, not the filesystem on it, and they mirror [`ls -l`](/peios/using-peios/peiosutils/listing-and-paths/ls.md) exactly — there is no `GROUP` column and no POSIX permission bits, because access to a device node is governed by its security descriptor, not by a mode. `OWNER` is the owner SID (`S-1-…`); `MODE` is a three-character `[type][x][+]`: the device-type character (`b` for a block device, `c` for a character device), an `x` slot (never set for a block device), and a `+` when the DACL is inheritance-protected. When the SD cannot be read — for instance on a non-Peios host with no KACS syscalls — `OWNER` shows `?` and `MODE` degrades honestly.

## Filtering, sorting and shaping

| Flag | Effect |
|---|---|
| `-a`, `--all` | Include empty (zero-size) devices, which are hidden by default. |
| `-d`, `--nodeps` | Do not print a device's holders or slaves (drop the children). |
| `-I`, `--include LIST` | Show only devices with these major numbers (comma-separated). |
| `-e`, `--exclude LIST` | Exclude devices by major number. The default is `1` (RAM disks); an explicit `-e` replaces that default, and `-a` clears exclusions entirely. |
| `-s`, `--inverse` | Print dependencies in inverse order (holders above the devices they depend on). |
| `-M`, `--merge` | Collapse a subtree shared by several parents (a multipath device) to a single occurrence. |
| `-E`, `--dedup COLUMN` | Drop rows whose `COLUMN` value duplicates an earlier one. |
| `-x`, `--sort COLUMN` | Sort siblings by `COLUMN` (numeric columns sort by value, not text). |

## Formatting

| Flag | Effect |
|---|---|
| `-b`, `--bytes` | Print `SIZE` as an exact byte count instead of a human-readable value. |
| `-p`, `--paths` | Print full `/dev` paths in the `NAME` column. |
| `-n`, `--noheadings` | Omit the header row. |
| `-i`, `--ascii` | Draw the tree with ASCII characters instead of box-drawing glyphs. |
| `-y`, `--shell` | Render column keys shell-safe (`MAJ:MIN` becomes `MAJ_MIN`). |
| `-w`, `--width NUM` | Truncate each table row to `NUM` columns wide. |
| `--sysroot DIR` | Read sysfs, the mount table and `/dev` from `DIR` instead of `/` (chiefly for testing against a fixture). |
| `-h`, `--help` / `-V`, `--version` | Standard. |

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Incorrect invocation (an unknown column, a malformed major-number or width value) or a failed run (for example, `/sys/block` is unreadable). |

`lsblk` uses this single non-zero code, matching util-linux. A per-device read failure is not fatal: it degrades the affected columns to empty or `?` and the run continues. A broken output pipe (piping into `head`, for instance) is treated as success.
