# Disks and filesystems

---

# Disks and filesystems

_Peios / Using Peios / Disks and filesystems_

> The tools Peios ships for creating, checking and inspecting ext2/3/4 and FAT filesystems, and why they are packaged from upstream rather than rewritten.

Creating a filesystem is the one operation that happens *before* Peios has any say over it. A block device holds bytes; a filesystem is a structure imposed on those bytes; only when that structure is mounted does the kernel begin making access decisions about what it contains. Everything on this page happens on the early side of that line.

Peios ships two upstream families: **e2fsprogs** for ext2/3/4, and **dosfstools** for FAT. Both are the tools you know from any Linux system. e2fsprogs carries a small Peios patchset, and the one place it diverges is security descriptors, which [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md) covers. dosfstools carries no functional patch at all, for a reason worth stating up front: a FAT filesystem has no extended attributes, so it has nowhere to keep a security descriptor and there is nothing for that work to extend.

## Why these tools are packaged, not rewritten

Most user-facing commands on Peios come from peiosutils, which reworks each tool for the Peios security model — `mount` grew a `policy=` option, `lsblk` reports SD-derived owner and mode, `ls` reads SDs rather than POSIX modes. `mkfs` and `fsck` are deliberately not in that set.

The reason is where the security seam falls. **`mount` is the seam**: it is the operation that takes a filesystem and places it under KACS, choosing the mount policy that governs every access from that point on. `mkfs` and `fsck` sit below the seam. They write and repair a Linux-compatible on-disk format that Peios deliberately mirrors byte for byte — an ext4 filesystem made on Peios is an ext4 filesystem, readable anywhere. Rewriting them would mean reimplementing a format Peios does not want to change, and taking on the correctness burden of a filesystem checker for no security benefit.

So Peios packages e2fsprogs from upstream and carries a patch series for the one thing upstream has no concept of.

## What ships

| Package | Contents |
|---|---|
| `e2fsprogs` | The tools below. |
| `e2fsprogs-devel` | Headers, linker symlinks and pkg-config files for building against the libraries. |
| `e2fsprogs-static` | Static archives. |
| `libext2fs`, `libe2p`, `libcom-err`, `libss`, `libuuid` | The runtime shared libraries, packaged separately so a consumer can depend on one without pulling the tools. |

The tools themselves:

| Tool | Purpose |
|---|---|
| `mke2fs`, `mkfs.ext2`, `mkfs.ext3`, `mkfs.ext4` | Create a filesystem. This is where Peios security descriptors enter. |
| `e2fsck`, `fsck.ext2`, `fsck.ext3`, `fsck.ext4`, `fsck` | Check and repair a filesystem. |
| `tune2fs` | Change parameters on an existing filesystem, including enabling features after the fact. |
| `resize2fs` | Grow or shrink a filesystem. |
| `dumpe2fs` | Print superblock and block-group information. |
| `debugfs` | Interactive low-level access to a filesystem image, including reading and writing extended attributes directly. |
| `blkid`, `findfs` | Identify filesystems by label, UUID or type. |
| `e2label`, `e2image`, `e2undo`, `e2freefrag`, `filefrag`, `badblocks`, `logsave` | Labelling, imaging, undo, fragmentation and block-scanning utilities. |
| `chattr`, `lsattr` | Read and set ext2/3/4 inode attributes. |
| `uuidgen` | Generate a UUID. |

`libuuid` comes from e2fsprogs; `libblkid` comes from the util-linux libraries. The split is arbitrary but fixed — each library has exactly one owning package, so the two sources never both ship the same file.

From `dosfstools`:

| Tool | Purpose |
|---|---|
| `mkfs.fat`, `mkfs.vfat`, `mkfs.msdos` | Create a FAT12/16/32 filesystem. |
| `fsck.fat`, `fsck.vfat`, `fsck.msdos` | Check and repair a FAT filesystem. |
| `fatlabel` | Read or set a FAT volume label. |

The pre-4.0 aliases (`mkdosfs`, `dosfsck`, `dosfslabel`) are deliberately not shipped — they exist for compatibility with a command-line history Peios does not have.

## Where these tools live

Everything above is in **`/usr/bin`**, reached as `/bin` through the runtime view. None of it is in `/sbin`: that is for daemons, which appear in service definitions, and a filesystem tool is a privileged binary a person runs.

The `fsck.<type>` backends are the exception, and they sit in **`/usr/libexec/fsck/`**:

```
/libexec/fsck/fsck.ext2   fsck.ext3   fsck.ext4
                fsck.fat    fsck.vfat   fsck.msdos
```

These are not commands you type. `fsck` picks a checker by exec'ing `fsck.<type>` for the filesystem type it detects or is given, so they are a private interface between the front-end and its backends — which is exactly what `libexec` distinguishes. `fsck` searches `/libexec/fsck` first, then the directories on your `PATH`, so a third-party checker installed elsewhere on `PATH` is still found.

The `mkfs.<type>` names stay in `/usr/bin` for the opposite reason: Peios ships no `mkfs` front-end, so nothing ever dispatches on them and they are only ever typed.

## FAT and the EFI system partition

The reason Peios ships FAT tooling at all is the **EFI system partition**. UEFI requires the ESP to be FAT, so a system that cannot format FAT cannot create its own boot partition.

An ESP is also the clearest case of a filesystem that holds no access control of its own. There is no extended-attribute channel, so no security descriptor is ever written to it, and none can be. Its entire access policy comes from the mount — necessarily one of the synthesising classes, since `facs_deny_missing` on a filesystem where every file is permanently missing an SD would make the whole partition unreachable. See [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

The practical consequence: the protection on your boot partition is the mount policy and the physical security of the disk, not a descriptor on the files. Treat the contents accordingly.

## Where this sits in a running system

`debugfs` is the tool to reach for when you want to inspect an image *offline* — without mounting it, and therefore without KACS being involved at all. It reads and writes extended attributes directly, which makes it the way to confirm that a security descriptor really landed on disk:

```
debugfs -R "ea_list <2>" /dev/vda2
```

Inode 2 is always the root directory of an ext filesystem, so this asks what extended attributes the filesystem's root carries.

`e2fsck` has a specific place in boot. Checking and repairing the root filesystem is the initramfs's job, not peinit's — by the time peinit runs, the root is already mounted, and a filesystem checker cannot repair a filesystem that is in use. See [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md).

## What is not here yet

**Partition tables other than GPT.** `part` writes GPT and only GPT. Peios boots through UEFI with no bootloader, so MBR has nothing to do on a Peios system — but a disk that already carries one is recognised and named rather than silently overwritten. Resizing, moving and MBR↔GPT conversion do not exist either. See [Partitioning](/peios/using-peios/disks-and-filesystems/partitioning.md).

**Filesystems other than ext2/3/4 and FAT.** The mount side understands XFS, Btrfs and NTFS as well — see [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md) — but Peios ships creation tools only for the ext and FAT families.

## Where to start

For the security-descriptor model — why a filesystem you intend to keep should carry a real SD from the moment it is created, and how the tree gets one — read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

For the exact command surface, including the extended options and the Peios defaults baked into the binary, read [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md).

For how these tools are put to work copying a live system onto a disk that then boots itself, read [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md).

For the names a disk keeps across reboots — `/dev/disk/by-uuid` and its siblings, and the device manager that maintains them — read [Stable device names](/peios/using-peios/disks-and-filesystems/stable-device-names.md).

For what happens to a filesystem once it is attached to the mount tree, read [Mount policies](/peios/using-peios/mount-policies/overview.md).

---

# Stable device names

_Peios / Using Peios / Disks and filesystems_

> Why /dev/vda is not a name to keep, what /dev/disk/by-uuid and its siblings are, who creates them, and where they are not available.

The kernel names a disk by the order it found it in: `/dev/vda`, `/dev/sda`, `/dev/nvme0n1`. That order is whatever the hardware produced this time — a second controller that probed faster, a USB stick that was plugged in, a disk that moved to another port — so the same disk is not guaranteed the same name on the next boot. A name of that kind is fine for a command you type now and wrong for anything written down: a boot cmdline, a mount reference, a script.

Peios keeps kernel names for the moment and gives every disk a set of **stable names** for everything else. They live under `/dev/disk/`, as symlinks to whatever kernel name the device happens to have:

| Directory | Keyed by | Example |
|---|---|---|
| `by-uuid` | the filesystem's UUID, written when it was formatted | `4ED7-A6AC -> ../../vda2` |
| `by-label` | the filesystem's label | `PEIOS -> ../../vda` |
| `by-partuuid` | the GPT partition entry's UUID | `2036ae68-…-f118299d5446 -> ../../vda2` |
| `by-partlabel` | the GPT partition entry's name | |
| `by-id` | the hardware's own identity: model and serial, or WWN | `nvme-QEMU_NVMe_Ctrl_peiosnvme1 -> ../../nvme0n1` |
| `by-path` | the bus position the device sits at | |
| `by-diskseq` | the kernel's monotonic disk sequence number, for this boot only | |

Which one to use depends on what you mean. A *filesystem* is `by-uuid` — it follows the data if the disk is cloned, and it is what `root=UUID=` on the kernel command line names. A *partition* independent of what is on it is `by-partuuid`. A *physical disk* whatever is written to it is `by-id`. `by-label` is the human-friendly one and the only one that is not unique by construction.

Partitions appear under the same keys with the parent's identity plus `-partN`, so `by-id/nvme-…-part2` is the second partition of that NVMe disk.

## Who creates them

The names are made by the **device manager**, eudev, which peinit starts first in phase 2 of boot. As each block device appears — at boot, when it replays the kernel's enumeration, and afterwards whenever a device is plugged in — the device manager probes it (its partition table, and the filesystem signature on each partition) and creates the matching links. Unplug the device and the links go away.

That is the same daemon that loads a driver for each device the kernel reports, so a disk behind a modular controller gets its driver, its kernel name and its stable names from one pass. See [Boot and boot modes](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md) for where the service sits in the boot order; a service that opens a device by stable name should `Requires` it, because peinit does not release eudev's dependents until the boot-time replay has finished.

The same idea applies to network interfaces. The kernel names them `eth0`, `eth1` in probe order; the device manager renames each one by where it sits — `enp0s3` for the card in PCI slot 3 on bus 0, `eno1` for an onboard port the firmware numbers — so a configuration written against the name survives a second card or a driver that probes faster next time. `net.ifnames=0` on the kernel command line turns the renaming off.

The device manager does not decide who may open a device. That is the security descriptor on the node, seeded on `/dev` before any of this runs — see [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

## Where they are not available

**The initramfs has none of them.** No device manager runs there — only a single pass that loads drivers, described in [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md). The initramfs still honours `root=UUID=`, but it resolves the UUID by probing each block device directly rather than by looking in `/dev/disk/by-uuid`, and `lsblk` does the same. That is why the installer's cmdline works on a machine the initramfs has never seen: it never depended on the links.

**`by-diskseq` does not survive a reboot.** The sequence number is assigned in the order devices appear during *this* boot, which is precisely the property the other directories exist to avoid depending on. It is there for tools that need to tell a re-plugged device from the one it replaced.

## Where to go next

For writing a partition table that gives every partition a `by-partuuid` name, read [Partitioning](/peios/using-peios/disks-and-filesystems/partitioning.md).

For how the installer records the root filesystem's UUID into the kernel command line, read [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md).

---

# Partitioning

_Peios / Using Peios / Disks and filesystems_

> part, the Peios partitioning tool: creating GPTs, adding and removing partitions, and what it refuses to touch.

A disk arrives from the factory as one undifferentiated run of sectors. Before a filesystem can live on it, something has to write down where each one begins and ends. That is a **partition table**, and on Peios the tool that writes one is `part`.

`part` manages **GPT** and nothing else. That is not an omission waiting to be filled: Peios boots through UEFI with no bootloader and no boot manager, so the MBR layout it would otherwise support has nothing to do on a Peios system.

## The shape of the tool

If you have used Windows, `part` occupies the slot `diskpart` occupies — but it is not the same shape, and the difference is deliberate.

| | `diskpart` | `part` |
|---|---|---|
| how you use it | an interactive shell: `select disk 0`, then act on it | one command, one device, one job |
| what it covers | partitions, formatting, drive letters, dynamic disks | the partition table |

Everything else `diskpart` bundles already has a home here: [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md) and `mkfs.vfat` make filesystems, `mount` mounts them, and Peios has no drive letters. And a `select`-then-act model is a hazard in a script, which is what usually calls `part` — a command that acts on "whatever was selected earlier" is one stray line away from acting on the wrong disk.

## Listing disks

Run `part list` with no arguments to see every disk on the machine:

```
# part list
DEVICE             SIZE  CONTENTS
/dev/vda           8.0G  gpt, 2 partitions
  vda1             512M  esp    EFI system partition
  vda2             7.5G  linux  Peios root
/dev/vdb           8.0G  no partition table
```

This is the view to start from, because it answers the question that precedes every other one: which disk did you mean? The partition names are the ones the kernel actually created — `vda1` on a virtio or SATA disk, `nvme0n1p1` on an NVMe one — so they are what you can pass straight to `mkfs` or to the installer.

The structure here comes from the kernel, not from reading a partition table, so a disk carrying a format `part` cannot manage still shows its partitions. A disk it cannot read at all is still listed, with `?` for its contents — the disk you cannot read is exactly the one worth knowing about.

Name a disk to see it in full:

```
# part list /dev/vda
/dev/vda: 16777216 sectors of 512 bytes (8.0G)
Label:     gpt
Disk GUID: DE942295-427C-411D-8023-B689D8BEE907
Usable:    34 .. 16777182

  #         START           END        SIZE  TYPE      NAME
  1          2048       1050623        512M  esp       EFI system partition
  2       1050624      16777182        7.5G  linux     Peios root

Free (aligned): 0B  in 0 extent(s), largest 0B
```

`Free (aligned)` counts space a new partition could actually occupy, which is not the same as unallocated sectors — the run below the first alignment boundary can never hold one. Reporting raw free space would promise room that `add` would then refuse to use.

`part verify` checks the same table's structure: both checksums, the two headers agreeing with each other, no overlaps, everything aligned and inside the usable range.

## Creating a table

```
# part create /dev/vda --yes
/dev/vda: wrote a new GPT (DE942295-427C-411D-8023-B689D8BEE907)

# part add /dev/vda --size 512M --type esp   --name "EFI system partition" --yes
/dev/vda: partition 1 at 2048..1050623 (512M)

# part add /dev/vda --size max  --type linux --name "Peios root" --yes
/dev/vda: partition 2 at 1050624..16777182 (7.5G)
```

`add` puts each partition in the first free run that fits, aligned to 1 MiB. `--size max` takes the largest free run rather than merely the last one, so it still does the obvious thing on a disk with a gap in the middle.

`part del /dev/vda 2 --yes` removes a partition by number. It frees the space and the slot; it does not touch the data that was in it.

### Sizes are sectors unless you say otherwise

`--size` takes `K`, `M`, `G`, `T` — powers of 1024 — or `max`. **A bare number is a sector count, not bytes.** `--size 2048` is 1 MiB on a 512-byte-sector disk, and you can write `2048s` to say so explicitly. Reading it as bytes would silently produce a partition a thousand times smaller than intended.

### Types

`--type` takes a short alias or a raw GUID:

| Alias | Meaning |
|---|---|
| `esp` | EFI system partition |
| `linux` | Linux filesystem data |
| `swap` | Linux swap |
| `msdata` | Microsoft basic data |

The list is short on purpose. Every type GUID in circulation would be a catalogue to keep current, and since a raw GUID is always accepted, nothing is unreachable for want of an alias.

### Names

Up to 36 UTF-16 code units — fewer if you use characters outside the Basic Multilingual Plane, which cost two units each. A longer name is **refused rather than shortened**. A partition name is how you identify the thing you are about to format, and a tool that quietly truncates it makes the label on your screen disagree with the label on the disk.

## What it will not do

`part` is the one tool on the system whose mistakes cannot be undone, so it is deliberately hard to point at the wrong thing.

**It requires `--yes`.** There is no interactive "are you sure": the usual caller is a script, and a prompt nobody can answer is worse than no prompt at all.

**It refuses a partition.** Naming `/dev/vda1` where you meant `/dev/vda` would write a GPT *inside* a partition — a table that looks valid to anything reading that partition directly, and is invisible to everything else.

```
# part create /dev/vda1 --yes
part: /dev/vda1 is a partition, not a whole disk; did you mean /dev/vda?
```

**It refuses a disk with anything mounted on it.** Not just the disk itself — any partition of it.

**It refuses a table it did not create.** This is the important one:

```
# part create /dev/vdb --yes
part: this disk carries an MBR (dos) partition table, which part cannot manage;
      pass --force to replace it — every partition on it will be lost
```

`part list` says what it *found*, not merely that it found no GPT — because "no GPT" is ambiguous between a blank disk and a disk holding somebody's data, and those deserve opposite treatment:

| What is there | What `part` says |
|---|---|
| an MBR | "an MBR (dos) partition table, which part cannot manage" |
| an Apple, BSD, Sun or SGI label | names it |
| a GPT whose header is corrupt | "the table may be damaged" |
| a filesystem written straight to the disk | "a `<type>` filesystem … with no partition table" |
| genuinely nothing | "no partition table" |

`--force` is the way through, and it is a *second* confirmation, separate from `--yes`. `--yes` means "I mean this destructive operation"; `--force` means "and I know it destroys a table that was already there". Requiring both is proportionate for an operation with no undo.

## Alignment, and why 1 MiB

Every partition starts on a 1 MiB boundary — 2048 sectors on a 512-byte-sector disk, 256 on a 4096-byte one. The number is not arbitrary: 1 MiB divides every erase block and RAID stripe width in practical use, so an aligned partition never straddles one. A misaligned filesystem pays a read-modify-write cycle on every boundary-crossing write, for the life of the filesystem.

`part` reads the logical sector size from the kernel rather than assuming it, so a 4Kn disk gets a correct table rather than one whose every structure is in the wrong place.

## Exit status

| Code | Meaning |
|---|---|
| 0 | success |
| 1 | usage error, or the operation failed |
| 2 | could not read or write the device |
| 3 | refused by a safety check |

**3 is separated from 1 on purpose.** A refusal is `part` working correctly, not malfunctioning, and a script should treat "this disk is not what you said it was" differently from "partitioning broke". `peios-install` relies on exactly this distinction.

## Where to go next

To put a filesystem on what you just created, read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md) and [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md).

To have the installer do all of this for you, read [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md) — `peios-install --whole-disk` runs exactly the three commands above before it formats anything.

---

# Formatting with security descriptors

_Peios / Using Peios / Disks and filesystems_

> How a filesystem gets real security descriptors at creation time, rather than synthesised ones at mount time, and why that distinction matters.

A freshly created filesystem contains one directory — its root — and that directory has no security descriptor. Nothing in the ext4 on-disk format has any concept of one. This is a problem the moment the filesystem is mounted under a FACS-managed policy, because [`facs_deny_missing`](/peios/using-peios/mount-policies/policy-classes.md) means exactly what it says: a file with no SD is unreachable, and that includes the root directory you are trying to enter.

There are two ways out. One is to let the kernel invent an SD at mount time. The other is to put a real one on the filesystem when it is created. Peios can do both, and which is appropriate depends on whether the filesystem is something you are passing through or something you intend to keep.

## Synthesised versus stamped

Mount-time synthesis is the right answer for media you do not own. A USB stick formatted on another system, a read-only image, an NTFS volume from Windows — these have no Peios SDs and should not acquire any. `facs_synthesize_ephemeral` gives every inode an SD in memory, derived from the mount's template, and never writes it back.

It is the wrong answer for a filesystem that is going to *be* a Peios system. A synthesised SD is a property of how the filesystem was mounted, not of the filesystem. Mount it with a different template and the whole tree's access policy changes; mount it somewhere that does not set a template and you get whatever the default is. The access policy of a system you own should live on that system, not in the command that attached it.

**Stamping** puts it there. `mke2fs` accepts a security descriptor at format time and writes it to the filesystem's root directory, so the filesystem carries its own policy from the moment it exists and mounts cleanly under `facs_deny_missing` with no template required.

## The root SD, and what it reaches

You give the descriptor as [SDDL](/peios/security-fundamentals/security-descriptors/overview.md), and `mke2fs` writes it to the `security.peios.sd` extended attribute on the root directory and on `lost+found`:

```
mke2fs -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)" /dev/vda2
```

That descriptor makes SYSTEM the owner and grants both SYSTEM and `BUILTIN\Administrators` full control. The `OICI` flags on each ACE — object-inherit and container-inherit — are what make it reach further than the root directory. Every file and directory subsequently created anywhere on the filesystem derives its own SD from that one through ordinary [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md).

That is worth stating plainly, because it cuts both ways. A single inheritable ACE on the root is, in practice, the access policy of the entire filesystem. It is a complete answer for a system tree where everything should be administrator-owned. It is not a way to express "readable system tree, private home directories" — one inherited ACL cannot say two different things, and the ACEs that would make `/home/alice` private have to come from somewhere else.

When you read an SD, treat the `ID` flag as a diagnostic: `ID` is `INHERITED_ACE`, so that descriptor was derived rather than stored. When you see it, the file's policy is not on the file — go and look at the ancestor it came from.

## Populating a tree with per-node descriptors

Inheritance from the root covers files created *on* the filesystem. It does not cover a tree copied *onto* it, where the nodes already have descriptors of their own that need preserving.

`mke2fs -d` populates a new filesystem from a directory on the build host, and on Peios it is security-descriptor aware. For each node it copies, it computes the SD the node should have in its new home, by combining two inputs:

- The **explicit descriptor** the source node carries — what the creator of that node wanted for it.
- The **parent's descriptor in the new filesystem**, which supplies the inheritable ACEs.

The two are merged by the same rules that govern inheritance on a live system: explicit ACEs first, inherited ACEs appended after them. A node whose source carries no explicit descriptor is left alone, and inherits normally on first access instead. A node whose parent has no descriptor keeps its explicit one verbatim.

The walk is top-down, and directories are stamped before their contents are visited, so every child reinherits against a parent whose descriptor has already been written.

### Why the source uses a different xattr

The explicit descriptor on the source tree is read from **`user.peios.sd`**, not `security.peios.sd`. This looks like an inconsistency and is not.

`security.peios.sd` is the canonical, on-disk home for a descriptor — and precisely because it is canonical, it is protected. On a live Peios filesystem the kernel seals it: direct extended-attribute operations on it are refused unconditionally, and access goes through `kacs_get_sd` and `kacs_set_sd` instead. On a Linux build host, writing anything in the `security.*` namespace needs `CAP_SYS_ADMIN`.

Neither is available to a tool staging a tree. `user.peios.sd` is subject to neither restriction, which makes it the portable carrier: any user can attach it, on any host, and it travels with the tree through ordinary archive and copy operations. `mke2fs -d` reads it, computes the result, and writes the answer to the canonical `security.peios.sd` on the new filesystem. It also skips both names when copying the node's other extended attributes across, so neither the carrier nor a stale canonical value is propagated verbatim.

## This all works offline

None of the above needs a running Peios kernel. The reinheritance computation is pure userspace, and `mke2fs` writes the extended attributes through the ext2 library, addressing the image directly rather than going through the host's filesystem layer. That bypasses the host's own security module and the Peios seal alike, for the simple reason that neither is in the path.

The practical consequence is that a Peios filesystem, with its full security policy in place, can be built on a machine that is not running Peios.

## Where to go next

For the exact syntax of the extended options and the defaults `mke2fs` applies on Peios, read [`mke2fs`](/peios/using-peios/disks-and-filesystems/mke2fs.md).

For how a descriptor is physically stored once written, including when an oversized one needs its own inode, read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

For what the mount policy does with the descriptor you stamped — and what happens on a filesystem that has none — read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

---

# mke2fs

_Peios / Using Peios / Disks and filesystems_

> The Peios additions to mke2fs: the root_sddl and root_sd_file extended options, security-descriptor-aware population, and the compiled-in Peios defaults.

`mke2fs` creates an ext2, ext3 or ext4 filesystem. Peios packages it from upstream e2fsprogs, so the generic surface — `-t`, `-b`, `-L`, `-O`, `-i`, `-m`, the full extended-option list — is exactly the upstream one and its canonical documentation is the `mke2fs(8)` man page shipped with the package.

This page documents only what Peios adds, which is security descriptors and a changed set of defaults.

```
mke2fs [-t ext4] [-E root_sddl=SDDL | root_sd_file=PATH] [-d DIRECTORY] DEVICE
```

## Extended options

Peios adds two options to `-E`. Both set the security descriptor written to the new filesystem's root directory and to `lost+found`, in the `security.peios.sd` extended attribute.

| Option | Meaning |
|---|---|
| `root_sddl=SDDL` | The descriptor as an inline SDDL string. |
| `root_sd_file=PATH` | The descriptor as SDDL read from `PATH`. |

`root_sd_file` exists because `-E` takes a comma-separated list and SDDL contains commas. Any descriptor with more than one ACE, or with a conditional expression, is easier to pass in a file than to quote on a command line.

The file holds **SDDL text, not binary** — it is the same string `root_sddl` would take, in a file. Trailing newlines are stripped, so an ordinary one-line text file works. Anything else in the file is part of the descriptor: there is no comment syntax and no blank-line handling.

Both options are parsed in the order they appear. If you give both, or the same one twice, the last one wins.

If the SDDL does not parse, `mke2fs` prints `Invalid root SDDL:` followed by the string it was given, and exits without creating a filesystem.

Neither option has a default. Omit both and the filesystem is created with no security descriptor on its root, which is upstream behaviour — mount it under a synthesising policy or it will be unreachable.

### Example

```
mke2fs -q -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)" /dev/vda2
```

Confirm the result without mounting the filesystem:

```
debugfs -R "ea_list <2>" /dev/vda2
```

```
Extended attributes:
  security.peios.sd (96)
```

Inode 2 is the root directory. To read the descriptor back as bytes rather than just confirm its presence, use `debugfs -R "ea_get -V <2> security.peios.sd"`.

## Security-descriptor-aware population

`-d DIRECTORY` populates the new filesystem from an existing directory. On Peios this pass also computes a security descriptor for each node it creates.

The source node's explicit descriptor is read from its **`user.peios.sd`** extended attribute — not `security.peios.sd`, which is sealed on a live Peios filesystem and requires `CAP_SYS_ADMIN` on a Linux build host. Both names are excluded from the generic extended-attribute copy, so neither is propagated verbatim.

For each node:

| Source node | Parent in the new filesystem | Result |
|---|---|---|
| Has `user.peios.sd` | Has a descriptor | Explicit and inherited ACEs merged, written to `security.peios.sd`. |
| Has `user.peios.sd` | Has none | The explicit descriptor written verbatim. |
| Has no `user.peios.sd` | Either | Nothing written. The node inherits on first access instead. |

Directories are stamped before their contents are visited, so a child always reinherits against a parent whose descriptor is already on disk.

The merge follows the same rules as [inheritance](/peios/security-fundamentals/security-descriptors/inheritance.md) on a live system, and runs entirely in userspace — no kernel and no KACS are involved, so a populated Peios filesystem can be built on a host that is not running Peios.

## Peios defaults

`mke2fs` reads its defaults from a profile. Peios changes three of them, and the changed profile is **compiled into the binary**, so it applies whether or not a configuration file exists.

| Setting | Upstream | Peios | Reason |
|---|---|---|---|
| `inode_size` | 256 | **512** | Keeps a typical security descriptor in the inode's inline extended-attribute space, where reading it costs no extra I/O. |
| `default_mntopts` | `acl,user_xattr` | **`user_xattr`** | Peios uses security descriptors, not POSIX ACLs. |
| ext4 features | — | **+ `ea_inode`** | Lets an oversized descriptor spill into its own inode rather than failing to fit. |

`base_features`, `blocksize`, `inode_ratio` and `enable_periodic_fsck` are unchanged from upstream.

The ext4 **`quota`** feature is deliberately *not* enabled. The Peios kernel is built with `CONFIG_QUOTA` and `CONFIG_QUOTACTL` but without `CONFIG_QFMT_V2`, and ext4 refuses to mount a filesystem carrying the quota feature unless the vfsv1 on-disk format is compiled in. Enabling it in the profile therefore produced filesystems the kernel would not mount. Turning it on again is a pair of changes that have to land together — the kernel option, then the feature.

Resolution order for the profile, highest first:

1. The file named by the `MKE2FS_CONFIG` environment variable, if set.
2. `/etc/mke2fs.conf`, if present.
3. The compiled-in Peios profile.

Peios ships no `/etc/mke2fs.conf`, so the compiled-in profile is what you get unless you deliberately supply a file. Supplying one replaces the profile wholesale — the compiled-in values are a fallback, not a layer underneath — so a partial configuration file silently reverts every setting it does not mention to the upstream default.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Failure. Includes an unparseable `root_sddl`/`root_sd_file` value, an unreadable `root_sd_file`, and a failure to write the descriptor to the new filesystem. |

`mke2fs` distinguishes no further; unlike `e2fsck`, it has no bitwise-summed status. When the security-descriptor options fail they report the offending value on standard error before exiting, and no filesystem is created.

## See also

For the model behind the two options — why stamping beats mount-time synthesis, and what one inheritable ACE on the root does and does not express — read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

For reading and rewriting descriptors on a mounted filesystem, read [The sd command](/peios/security-fundamentals/security-descriptors/sd-command.md).

For how the descriptor is stored, and when `ea_inode` becomes load-bearing, read [SD storage by filesystem](/peios/using-peios/mount-policies/sd-storage-by-filesystem.md).

---

# Installing to disk

_Peios / Using Peios / Disks and filesystems_

> How a Peios system is copied from a live medium onto a disk, and how the installed system boots itself afterwards.

A live Peios runs from a read-only squashfs with a tmpfs stacked on top, so every write it accepts is discarded at reboot. Installing to disk replaces that arrangement with a writable filesystem that survives — and, less obviously, replaces a security policy chosen at *mount* time with one the filesystem carries itself.

Installation is five steps and one retirement. Nothing about it is magic, and all of it can be done by hand.

## Two ways to invoke it

```
peios-install --yes --whole-disk /dev/vda      # partition the disk, then install
peios-install --yes /dev/vda1 /dev/vda2        # install onto partitions that exist
```

The first form runs [`part`](/peios/using-peios/disks-and-filesystems/partitioning.md) before anything else — a fresh GPT, a 512 MiB ESP, and a root filling the remainder — and then proceeds exactly as the second. **The whole disk is erased.**

If the disk already carries a partition table `part` did not create, the install stops rather than overwriting it; `--force` is how you say you meant it. That refusal happens before the first `mkfs`, so nothing has changed when it does.

The second form is for any layout other than the one above: partition however you like with `part`, then name the two partitions.

## What the installer does

**1. Format the EFI system partition.** UEFI requires FAT, so this is `mkfs.vfat -F 32`. A FAT filesystem cannot hold a security descriptor and never will, so the ESP's access policy comes entirely from its mount — necessarily one of the synthesising classes.

**2. Format the root, with a descriptor.** This is the step with no equivalent on other systems:

```
mke2fs -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)" /dev/vdb2
```

The descriptor is written to the root directory's `security.peios.sd` at format time, so the filesystem is administrable from the instant it exists. Because both ACEs are inheritable, everything created inside it derives its own descriptor from that one. See [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

**3. Copy the system.** `cp -ax`, which preserves owner, DACL, SACL, timestamps, links and extended attributes, and stops at filesystem boundaries. Every one of those is required rather than best-effort, so a descriptor that cannot be carried across stops the install instead of quietly downgrading it. Mountpoints are recreated as empty directories rather than copied into: `/proc`, `/sys` and `/dev` are mount-moved into the new root by prelude at boot, and `/bin`, `/etc`, `/lib` and the rest are StrataFS views mounted over theirs. What lives *behind* those views — `/usr` and `/lcl` — is ordinary content on the root filesystem, and is copied in full.

**4. Write the kernel command line.** The installed system needs `root=`, naming the filesystem that did not exist until step 2. That cannot be package data, so it is generated: the `disk-boot` package ships a template at `/usr/share/disk-boot/cmdline` carrying everything stable, and the installer appends `root=UUID=<the new root>` and writes the result to `/lcl/etc/boot/cmdline` on the target. Package data supplies the general part; the operator tree holds the per-install part.

**5. Build the boot artifact.** `mkuki` bundles the kernel, the initramfs and that command line into a single EFI binary, written to the ESP at `EFI/BOOT/BOOTX64.EFI`.

That last path is the removable-media fallback, which UEFI firmware boots without an NVRAM entry. A Peios installation therefore needs **no bootloader and no boot manager** — there is nothing between the firmware and the kernel.

## What it refuses before it starts

Both partitions you name are about to be formatted, so the installer checks them before it does anything irreversible. It stops if either is not a block device, if you name the same partition twice, or — the one worth stating plainly — **if either is currently mounted**:

```
# peios-install --yes /dev/vda1 /dev/vda2
peios-install: /dev/vda2 is mounted; refusing to format it
```

That last check is what stands between you and naming the partition you are running from. All of it happens before the first `mkfs`, so a rejected install has changed nothing.

## The first-account service is retired, the account is not

Your accounts come across. What does not is the thing that *creates* them.

A live image ships `lpsd-first-account`: a oneshot service that runs `lps add` at boot to create the development account. It is written for exactly one situation, which is a live image — the root there is a tmpfs, so every boot starts from an empty store and the provisioner has work to do.

An installed system is the other situation. The accounts themselves live in lpsd's store at `/var/state/lpsd/principals`, which is ordinary content on the root filesystem and is copied like everything else. So the installed machine already has the account before it first boots, and re-running a provisioner against a populated store is not a harmless no-op: the script carries no idempotence guard, so `lps add` fails on the name that already exists and the service crashes on every boot. Once installation grows a "choose a password" step, it would be worse than noisy — a provisioner that reasserts the image's credential would undo the operator's choice at the next reboot.

So before it copies anything, the installer deletes the service from the registry:

```
reg del 'Machine\System\Services\lpsd-first-account'
```

Note *which* registry. It removes the key from the **live** system it is running on, not from the target — the registry is a live database served by `registryd`, and the only thing that can safely edit it is the `registryd` currently holding it open, so the removal happens at the source and the copy never carries it. Running the installer again, or running it from an already-installed system, finds nothing to remove and says so. The removal is checked afterwards and a failure stops the install, which happens before either partition is formatted and therefore costs nothing.

> [!CAUTION]
> One thing this deliberately does not solve: the account arrives with the password it had, and on an image that shipped a development account that password is in the image and therefore public. Until installation can prompt for a new one, treat a freshly installed system as carrying a known credential and change it — `lps` is the tool — before the machine is anywhere it matters.

## Use UUIDs, not device names

Step 4 records the root by UUID, and the reason is worth understanding rather than copying.

The disk that is `/dev/vdb` while you install — second device, behind the install medium — is `/dev/vda` when you boot it with the medium removed. A device name baked into the command line is a name for *where a disk was plugged in*, not for the disk, and it stops being true the moment the arrangement changes.

The initramfs resolves the UUID by probing block devices directly rather than reading `/dev/disk/by-uuid`, because no device manager runs in the initramfs and those directories do not exist there — see [Stable device names](/peios/using-peios/disks-and-filesystems/stable-device-names.md).

## How the installed system boots

The initramfs contains **two** hooks that can mount a root, and both are present in every image:

| Hook | Mounts |
|---|---|
| `mount-root.sh` | a live medium's squashfs, with a tmpfs overlay above it |
| `mount-root-disk.sh` | an installed root partition, directly |

Both run at every boot. `root=` on the command line decides which one acts; the other exits successfully having done nothing. [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md) covers the mechanism.

Two differences in what the disk hook does are worth calling out, because they are the point of installing at all.

**It mounts the partition directly — no overlay.** The live path stacks a tmpfs because the layer beneath it is read-only. An installed root is writable, so an overlay would serve only to throw away everything written to it.

**It mounts `policy=deny-missing`, and runs no `seed-sd`.** A live root has no descriptors at all — the squashfs ships none — so the live system mounts it `synth-ephemeral` and has KACS invent one per inode, then seeds a single inheritable descriptor onto the tmpfs above. An installed root needs none of that: it carries a real descriptor, written at format time, so a file *without* one is a fault worth surfacing rather than a gap to paper over.

This is the substantive difference between the two, and it is easy to check on a running installed system:

```
# sd show /
  Owner:      LocalSystem (S-1-5-18)
  DACL: (2 ACEs)
    [0] allow LocalSystem (S-1-5-18)                   0x10000000  [CI,OI]
    [1] allow BUILTIN\Administrators (S-1-5-32-544)    0x10000000  [CI,OI]
```

No `ID` flag on either ACE. `ID` is `INHERITED_ACE`, so its absence means this descriptor is *explicit* — stored on the root inode by `mke2fs`, not derived from an ancestor and not synthesised at mount. The filesystem's access policy is a property of the filesystem.

## What the installer does not do

**It does not choose your partition layout.** `--whole-disk` writes one specific arrangement — a 512 MiB ESP and a root filling everything else — and that is all it will ever write. Anything else is a job for [`part`](/peios/using-peios/disks-and-filesystems/partitioning.md) followed by the two-partition form of the installer.

**It does not choose a layout.** One ESP, one root, no separate `/home`, no swap, no encryption. Each of those is a real thing to want and none of them exists yet.

**It does not express a per-directory access policy.** The whole installed tree inherits from that single descriptor on the root. That is a complete answer for a system tree where everything is administrator-owned, and it is not a way to say "readable system tree, private home directories" — one inheritable ACL cannot say two different things. Per-subtree descriptors are the work that makes that expressible.

## Where to go next

For the format-time descriptor and how a populated tree gets its own, read [Formatting with security descriptors](/peios/using-peios/disks-and-filesystems/formatting-with-security-descriptors.md).

For the hook mechanism the two root-mounting hooks share, read [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md).

For what `deny-missing` does with a file that has no descriptor, and why the live path cannot use it, read [Policy classes](/peios/using-peios/mount-policies/policy-classes.md).

---

# StrataFS

_Peios / Using Peios / Disks and filesystems / StrataFS_

> StrataFS merges ordinary directories by precedence while keeping each stratum independently manageable. Mounting a stack and the stratafs inspection tool.

StrataFS presents several ordinary directories as one merged directory tree.
The directories remain ordinary and independently manageable at their real
paths; the StrataFS mount only supplies the merged view.

Strata are ordered from highest to lowest precedence. For each name, the first
stratum that holds it is its **provider**. Directories at the same name merge,
so their children are resolved in the same precedence order. A non-directory
provider masks every lower object at that name, including a lower directory's
whole subtree.

## A `/bin` example

Suppose `/usr/bin` contains packaged programs and `/lcl/bin` is for local
changes. Mount this stack at `/bin`:

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

`/lcl/bin` has higher precedence and receives newly-created objects. The `+ro`
on `/usr/bin` means “do not modify this stratum **through `/bin`**”. It does not
make the real `/usr/bin` mount read-only: an authorised writer can still modify
`/usr/bin/tool` directly at `/usr/bin/tool`.

Reading `/bin/tool` uses `/lcl/bin/tool` when it exists, otherwise
`/usr/bin/tool`. Writing an existing packaged tool through `/bin` copies it to
`/lcl/bin` first and changes the copy. Removing that copy through `/bin`
restores the unchanged `/usr/bin` version to view, because StrataFS does not use
whiteouts.

## The base Peios topology

The `fsbase` package installs the `mount-rootfs-stratafs-base.sh` [boot hook](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md)
in the initramfs. It runs after the deployment-specific hook has mounted the
real root and before prelude hands off to it, mounting the conventional
root-level views as one boot step:

| View | Strata, highest precedence first |
|---|---|
| `/bin` | `/lcl/bin+create`, `/usr/bin+ro+am` |
| `/sbin` | `/lcl/sbin+create`, `/usr/sbin+ro+am` |
| `/lib` | `/lcl/lib+create`, `/usr/lib+ro` |
| `/libexec` | `/lcl/libexec+create`, `/usr/libexec+ro+am` |
| `/share` | `/lcl/share+create`, `/usr/share+ro+am` |
| `/include` | `/lcl/include+create`, `/usr/include+ro+am` |
| `/etc` | `/system/retc`, `/lcl/etc+create`, `/usr/etc+ro+am` |
| `/conf` | `/lcl/conf+create`, `/usr/conf+ro+am` |

`am` permits an optional vendor directory to be absent when the system boots
and makes it participate automatically if a later package creates it. The
operator create directories are provisioned by `fsbase`; their absence is a
boot error rather than something StrataFS silently creates, because their own
security descriptors govern creation through each view.

`/lib` views `/usr/lib` rather than the architecture triplet directory beneath
it, so `/lib/modules` and `/lib/firmware` resolve. Both matter: kmod has
`/lib/modules` compiled in, and the kernel's firmware loader searches
`/lib/firmware`, and neither can be told to look elsewhere. Shared libraries are
unaffected — the loader finds them through its own absolute system search path
rather than through this view — and `/lib/x86_64-linux-peios/` still resolves,
one level down, which is the shape a foreign binary expects.

`/lib64` is not a StrataFS view. On x86-64 it remains the package-owned relative
symlink `lib64 -> usr/lib/x86_64-linux-peios`, because the psABI dynamic-loader
path must work before any hook can mount the base topology. It is a distinct
object from the `/lib` view above and is unaffected by what that view maps.

The mount option grammar is:

```text
strata=<stratum>[:<stratum>]...
<stratum> := <path>[+<flag>]...
<flag> := create | ro | am
```

Paths are absolute. `create` selects the one stratum that receives creations
and copy-up, `ro` prevents modification through the merged view, and `am`
allows the stratum directory itself to be temporarily absent. Literal `:`,
`+`, `,`, and `\` in a path are escaped with `\`.

## Inspecting a stack

The `stratafs` command is a read-only inspector. It does not mount, modify, or
clean anything, and it never gains extra authority. Every direct stratum read
runs with your normal access rights. If it cannot see all the information
needed for a complete answer—especially every participant of a protected
merged directory—it fails instead of showing a misleading partial result.

List every StrataFS mount in the current mount namespace, or one exact mount:

```sh
stratafs list
stratafs list /bin
```

Typical output is:

```text
/bin
  [0] /lcl/bin+create (present)
  [1] /usr/bin+ro (present)
```

The order is the precedence order. A stratum is `present`, `absent`, or
`not_directory`; the mount itself is also labelled when generically mounted
read-only.

## Explaining one path

Use `resolve` when you want to know why a name looks the way it does and where
a mutation would go:

```sh
stratafs resolve /bin/sh
```

The per-stratum states are:

| State | Meaning |
|---|---|
| `provider` | This object currently provides the name. |
| `participant` | This lower directory contributes to the merged directory. |
| `shadowed` | A higher object of the same type wins. |
| `masked` | A provider of another type hides this object. |
| `absent` | This stratum does not hold the path. |

The report also explains `write` and `delete`. A write can route `in_place`,
`copy_up`, or `create`; it can instead report `erofs`, a missing parent, an
unknown immutable-attribute state, or that content I/O follows a symlink.
Deletion identifies the real provider entry to remove and names a lower object
that will resurface. Removing a directory is still conditional on the complete
merged directory being empty.

These are routing answers, not authorisation promises. KACS checks the actual
operation against the caller when it is attempted.

For the kernel's answer without the explanation, print the synthetic origin
attribute:

```sh
stratafs origin /bin/sh
```

A non-directory prints its one real provider path. A merged directory prints
every participating real directory in precedence order, one escaped path per
line.

## Inspecting local state

The create stratum is deliberately easy to audit. `sweep` recursively reports
every object in it:

```sh
stratafs sweep /bin
```

Each result is classified as:

- `gap`: only the create stratum has this path;
- `override`: a lower stratum also has it; or
- `shadowed`: a higher stratum, or a higher non-directory ancestor, makes it
  unreachable.

Directories are included. For a directory, `override` describes structural
presence; corresponding directories still merge. When `sweep` says
`create stratum is empty`, the local stratum has no entries to reconcile. The
command never deletes anything—remove a reviewed entry through the merged view
or at its real create-stratum path, according to the result you intend.

To inspect the content of an override:

```sh
stratafs diff /bin/sh
```

`diff` compares the create-stratum object with the first lower default. It
supports regular files and symbolic links, reports type changes, refuses other
object types, and bounds each regular-file read to 16 MiB. Binary files are
reported only as different.

## Structured output and status

`list`, `resolve`, and `sweep` accept `--json`. JSON is the stable scripting
interface; it includes the same paths, flags, states, object types, and routing
actions. A non-UTF-8 path is represented losslessly in human output but causes
JSON mode to fail rather than substitute a lossy name.

Exit status has probe-friendly meaning:

| Status | Meaning |
|---|---|
| `0` | Success; for `sweep`, no entries; for `diff`, no difference. |
| `1` | `sweep` found entries or `diff` found a difference. |
| `2` | Usage, visibility, malformed-state, or operational error. |

## Where to go next

For the generic command that creates this and other mounts, read
[`mount`](/peios/using-peios/mount-policies/mount.md). For how access to every real object is
decided, start with [File access](/peios/security-fundamentals/file-access/overview.md).
