Disks and filesystems
Single-page view · as markdown
Disks and filesystems
Peios / Using Peios / Disks and filesystems
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 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.
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.
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.
Filesystems other than ext2/3/4 and FAT. The mount side understands XFS, Btrfs and NTFS as well — see SD storage by filesystem — 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.
For the exact command surface, including the extended options and the Peios defaults baked into the binary, read mke2fs.
For how these tools are put to work copying a live system onto a disk that then boots itself, read Installing to disk.
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.
For what happens to a filesystem once it is attached to the mount tree, read Mount policies.
Stable device names
Peios / Using Peios / Disks and filesystems
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 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.
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. 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.
For how the installer records the root filesystem's UUID into the kernel command line, read Installing to disk.
Partitioning
Peios / Using Peios / Disks and filesystems
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 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 and mke2fs.
To have the installer do all of this for you, read Installing to disk — peios-install --whole-disk runs exactly the three commands above before it formats anything.
Formatting with security descriptors
Peios / Using Peios / Disks and filesystems
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 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, 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.
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.
For how a descriptor is physically stored once written, including when an oversized one needs its own inode, read SD storage by filesystem.
For what the mount policy does with the descriptor you stamped — and what happens on a filesystem that has none — read Policy classes.
mke2fs
Peios / Using Peios / Disks and filesystems
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 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:
- The file named by the
MKE2FS_CONFIGenvironment variable, if set. /etc/mke2fs.conf, if present.- 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.
For reading and rewriting descriptors on a mounted filesystem, read The sd command.
For how the descriptor is stored, and when ea_inode becomes load-bearing, read SD storage by filesystem.
Installing to disk
Peios / Using Peios / Disks and filesystems
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 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.
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.
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.
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 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 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.
For the hook mechanism the two root-mounting hooks share, read Boot hooks.
For what deny-missing does with a file that has no descriptor, and why the live path cannot use it, read Policy classes.
StrataFS
Peios / Using Peios / Disks and filesystems / StrataFS
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:
/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
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:
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:
Typical output is:
/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:
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:
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:
Each result is classified as:
gap: only the create stratum has this path;override: a lower stratum also has it; orshadowed: 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:
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. For how access to every real object is
decided, start with File access.